无法使用Json.net覆盖Dictionary键序列化/反序列化

时间:2016-09-06 10:39:11

标签: .net json.net

我有一个包含IDictionary<decimal, int>属性的简单类型:

    public class CountEvent
{
    [JsonProperty(PropertyName = "_ts")]
    public DateTime Timestamp { get; set; }
    public String Pair { get; set; }
    [JsonProperty(PropertyName = "dir")]
    public String Direction { get; set; }
    public IDictionary<Decimal, Int32> Data { get; set; }

    public RateCountEvent()
    {
        Data = new Dictionary<Decimal, Int32>();
    }
}

我故意使用IDictionary,因为我在运行时提供了Dictionary或SortedDictionary实例。将属性类型更改为Dictionary类不会影响行为。
我想为Decimal提供自定义序列化逻辑(即删除尾随零),并为此编写了一个类:

    public class DecimalWithoutTrailingZerosConverter: JsonConverter
{
    private readonly IFormatProvider formatProvider;

    public DecimalWithoutTrailingZerosConverter(IFormatProvider formatProvider)
    {
        this.formatProvider = formatProvider;
    }

    public override Boolean CanConvert(Type objectType)
    {
        return objectType == typeof(Decimal);
    }

    public override Object ReadJson(JsonReader reader, Type objectType, Object existingValue, JsonSerializer serializer)
    {
        if (reader.TokenType != JsonToken.String)
            throw new Exception("Wrong Token Type");

        return Convert.ToDecimal(reader.Value, formatProvider);
    }

    public override void WriteJson(JsonWriter writer, Object value, JsonSerializer serializer)
    {
        String val;

        if (value is decimal)
        {
            val = ((Decimal) value).ToString("G29", formatProvider);
        }
        else
        {
            throw new Exception("Expected date object value.");
        }
        writer.WriteValue(val);
    }
}

是的,我知道JSON序列化通常是文化不变的。这不是我的观点。
问题是,当我将转换器添加到序列化设置时,它被认为是CanConvert方法被调试器命中),但是十进制类型从未在那里显示为{{1参数,而所有其他类型都是(我的类型CountEvent,DateTime,String,Dictionary(我认为它考虑运行时类型)和Int32)。
我查看了库代码,似乎objectType类中有一个自定义逻辑序列化字典(我假设)。
问题是 - 我看到的行为是正确的吗?可以覆盖吗?
附: 我不是说图书馆是错的(毕竟,成千上万的人每天都在使用它),我只是想找到一种方法让它适用于这种情况。

1 个答案:

答案 0 :(得分:1)

您的DecimalWithoutTrailingZerosConverter未用于字典键的原因是Json.NET没有序列化键 - 它只是将它们转换为字符串。来自docs

  

序列化字典时,字典的键将转换为字符串并用作JSON对象属性名称。为密钥编写的字符串可以通过覆盖密钥类型的ToString()或通过实现TypeConverter来自定义。在反序列化字典时,TypeConverter还将支持再次转换自定义字符串。

因此,为了获得您需要的输出,您可以按照here覆盖TypeConverter的系统decimal - 但我不会真的推荐它,因为这会改变转换器用于应用程序中的decimal,带来各种不可预见的后果。

替代方法是为所有IDictionary<decimal, TValue> TValue实施public class DecimalDictionaryWithoutTrailingZerosConverter : DecimalWithoutTrailingZerosConverterBase { public DecimalDictionaryWithoutTrailingZerosConverter(IFormatProvider formatProvider) : base(formatProvider) { } public override Boolean CanConvert(Type objectType) { var types = objectType.GetDictionaryKeyValueTypes().ToList(); return types.Count == 1 && types[0].Key == typeof(Decimal); } object ReadJsonGeneric<TValue>(JsonReader reader, Type objectType, IDictionary<decimal, TValue> existingValue, JsonSerializer serializer) { if (reader.TokenType == JsonToken.Null) return null; if (reader.TokenType != JsonToken.StartObject) throw new JsonSerializationException("Invalid object type " + reader.TokenType); if (existingValue == null) { var contract = serializer.ContractResolver.ResolveContract(objectType); existingValue = (IDictionary<decimal, TValue>)contract.DefaultCreator(); } while (reader.Read()) { switch (reader.TokenType) { case JsonToken.Comment: break; case JsonToken.PropertyName: { var name = reader.Value.ToString(); var key = TokenToDecimal(JsonToken.String, name); if (!reader.Read()) throw new JsonSerializationException(string.Format("Missing value at path: {0}", reader.Path)); var value = serializer.Deserialize<TValue>(reader); existingValue.Add(key, value); } break; case JsonToken.EndObject: return existingValue; default: throw new JsonSerializationException(string.Format("Unknown token {0} at path: {1} ", reader.TokenType, reader.Path)); } } throw new JsonSerializationException(string.Format("Unclosed object at path: {0}", reader.Path)); } public override Object ReadJson(JsonReader reader, Type objectType, Object existingValue, JsonSerializer serializer) { if (reader.TokenType == JsonToken.Null) return null; try { var keyValueTypes = objectType.GetDictionaryKeyValueTypes().Single(); // Throws an exception if not exactly one. var method = GetType().GetMethod("ReadJsonGeneric", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public); var genericMethod = method.MakeGenericMethod(new[] { keyValueTypes.Value }); return genericMethod.Invoke(this, new object[] { reader, objectType, existingValue, serializer }); } catch (Exception ex) { if (ex is JsonException) throw; // Wrap the TypeInvocationException in a JsonSerializerException throw new JsonSerializationException("Failed to deserialize " + objectType, ex); } } void WriteJsonGeneric<TValue>(JsonWriter writer, IDictionary<decimal, TValue> value, JsonSerializer serializer) { writer.WriteStartObject(); foreach (var pair in value) { writer.WritePropertyName(DecimalToToken(pair.Key)); serializer.Serialize(writer, pair.Value); } writer.WriteEndObject(); } public override void WriteJson(JsonWriter writer, Object value, JsonSerializer serializer) { try { var keyValueTypes = value.GetType().GetDictionaryKeyValueTypes().Single(); // Throws an exception if not exactly one. var method = GetType().GetMethod("WriteJsonGeneric", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public); var genericMethod = method.MakeGenericMethod(new[] { keyValueTypes.Value }); genericMethod.Invoke(this, new object[] { writer, value, serializer }); } catch (Exception ex) { if (ex is JsonException) throw; // Wrap the TypeInvocationException in a JsonSerializerException throw new JsonSerializationException("Failed to serialize " + value, ex); } } } public class DecimalWithoutTrailingZerosConverter : DecimalWithoutTrailingZerosConverterBase { public DecimalWithoutTrailingZerosConverter(IFormatProvider formatProvider) : base(formatProvider) { } public override Boolean CanConvert(Type objectType) { return objectType == typeof(Decimal); } public override Object ReadJson(JsonReader reader, Type objectType, Object existingValue, JsonSerializer serializer) { return TokenToDecimal(reader.TokenType, reader.Value); } public override void WriteJson(JsonWriter writer, Object value, JsonSerializer serializer) { writer.WriteValue(DecimalToToken(value)); } } public abstract class DecimalWithoutTrailingZerosConverterBase : JsonConverter { private readonly IFormatProvider formatProvider; public DecimalWithoutTrailingZerosConverterBase(IFormatProvider formatProvider) { this.formatProvider = formatProvider; } protected string DecimalToToken(decimal value) { return value.ToString("G29", formatProvider); } protected string DecimalToToken(object value) { if (value is decimal) { return DecimalToToken((Decimal)value); } else { throw new JsonSerializationException("Expected date object value."); } } protected decimal TokenToDecimal(JsonToken tokenType, object value) { if (tokenType != JsonToken.String) throw new JsonSerializationException("Wrong Token Type"); return Convert.ToDecimal(value, formatProvider); } } public static class TypeExtensions { /// <summary> /// Return all interfaces implemented by the incoming type as well as the type itself if it is an interface. /// </summary> /// <param name="type"></param> /// <returns></returns> public static IEnumerable<Type> GetInterfacesAndSelf(this Type type) { if (type == null) throw new ArgumentNullException(); if (type.IsInterface) return new[] { type }.Concat(type.GetInterfaces()); else return type.GetInterfaces(); } public static IEnumerable<KeyValuePair<Type, Type>> GetDictionaryKeyValueTypes(this Type type) { foreach (Type intType in type.GetInterfacesAndSelf()) { if (intType.IsGenericType && intType.GetGenericTypeDefinition() == typeof(IDictionary<,>)) { var args = intType.GetGenericArguments(); if (args.Length == 2) yield return new KeyValuePair<Type, Type>(args[0], args[1]); } } } } 的所有词典编写custom JsonConverter

var culture = new CultureInfo("de-DE");
var settings = new JsonSerializerSettings
{
    Converters = new JsonConverter[] { new DecimalWithoutTrailingZerosConverter(culture), new DecimalDictionaryWithoutTrailingZerosConverter(culture) },
    Formatting = Formatting.Indented,
};
var json = JsonConvert.SerializeObject(rateCountEvent, settings);

然后使用它:

existingValue

请注意,对所有类型的十进制字典使用单个转换器。另请注意,转换器使用RateCountEvent(如果存在)。因此,如果DecimalWithoutTrailingZerosConverter中的构造函数分配排序的字典而不是字典,则将填充已排序的字典。

示例fiddle

顺便说一句,您可能希望将decimal?扩展为处理decimal以及{{1}}。