【问题标题】:Can't override Dictionary keys serialization/deserialization with Json.net无法使用 Json.net 覆盖字典键序列化/反序列化
【发布时间】:2016-09-07 06:27:52
【问题描述】:

我有一个包含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 方法被调试器命中),但 Decimal 类型从不显示为 objectType 参数,而所有其他类型都可以(我的类型 CountEvent、DateTime、String、Dictionary(我认为它考虑运行时类型)和 Int32)。
我查看了库代码,似乎KeyValuePairConverter 类中有一个自定义逻辑可以序列化字典(我假设)。
问题是 - 我看到的行为对吗?它可以被覆盖吗?
附: 我并不是说这个库有问题(毕竟,每天都有成千上万的人使用它),我只是想找到一种方法让它适用于这种情况。

【问题讨论】:

  • 您的班级CountEventctor 不正确。
  • 你也永远不会发送 decimal ,而是字典。
  • @AmitKumarGhosh 怎么会这样? ctor 应该为空?
  • 关于从不发送小数 - 我明白这一点,但 CanConvert 方法考虑了 Int32 类型。 Decimal 不应该也是?
  • 显然Int32Dictionary&lt;decimal, int&gt; 无关。所有属性 getter 都因其类型而被调用,但不是因为最后一个 Int32 考虑。

标签: .net json.net


【解决方案1】:

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

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

因此,为了获得您需要的输出,您可以按照here 的说明覆盖系统TypeConverterdecimal - 但我不会真正推荐它,因为这会更改用于decimal 的转换器在您的应用程序的任何地方,都会产生各种无法预料的后果。

另一种方法是为所有实现IDictionary&lt;decimal, TValue&gt; 的字典写一个custom JsonConverter,对于一些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]);
            }
        }
    }
}

然后像这样使用它:

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 中的构造函数分配了一个排序字典而不是一个字典,则排序字典将被填充。

示例fiddle

顺便说一句,您可能希望扩展您的 DecimalWithoutTrailingZerosConverter 以处理 decimal?decimal

【讨论】:

  • 在朋友的帮助下,问题以类似的方式解决了。谢谢
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-12-10
  • 2014-02-19
  • 2011-09-18
  • 2014-09-01
  • 1970-01-01
相关资源
最近更新 更多