Newtonsoft Json 将字典反序列化为来自 DataContractJsonSerializer 的键/值列表

Posted

技术标签:

【中文标题】Newtonsoft Json 将字典反序列化为来自 DataContractJsonSerializer 的键/值列表【英文标题】:Newtonsoft Json Deserialize Dictionary as Key/Value list from DataContractJsonSerializer 【发布时间】:2015-04-11 16:25:57 【问题描述】:

我有一个使用 DataContractJsonSerializer 序列化到存储的字典,我想使用 Newtonsoft.Json 对其进行反序列化。

DataContractJsonSerializer 已将 Dictionary 序列化为键/值对列表:

"Dict":["Key":"Key1","Value":"Val1","Key":"Key2","Value":"Val2"]

我可以给JsonConvert.DeserializeObject<>() 提供什么很酷的选项,使其支持该数据格式和来自 Newtonsoft.Json 的格式?

"Dict":"Key1":"Val1","Key2":"Val2"

是 Newtonsoft.Json 创建的漂亮格式,我希望在过渡期间能够同时读取旧的 DataContract 格式和新的 Newtonsoft 格式。

简化示例:

    //[JsonArray]
    public sealed class Data
    
        public IDictionary<string, string> Dict  get; set; 
    

    [TestMethod]
    public void TestSerializeDataContractDeserializeNewtonsoftDictionary()
    
        var d = new Data
        
            Dict = new Dictionary<string, string>
            
                "Key1", "Val1",
                "Key2", "Val2",
            
        ;

        var oldJson = String.Empty;
        var formatter = new DataContractJsonSerializer(typeof (Data));
        using (var stream = new MemoryStream())
        
            formatter.WriteObject(stream, d);
            oldJson = Encoding.UTF8.GetString(stream.ToArray());
        

        var newJson = JsonConvert.SerializeObject(d);
        // [JsonArray] on Data class gives:
        //
        // System.InvalidCastException: Unable to cast object of type 'Data' to type 'System.Collections.IEnumerable'.

        Console.WriteLine(oldJson);
        // This is tha data I have in storage and want to deserialize with Newtonsoft.Json, an array of key/value pairs
        // "Dict":["Key":"Key1","Value":"Val1","Key":"Key2","Value":"Val2"]

        Console.WriteLine(newJson);
        // This is what Newtonsoft.Json generates and should also be supported:
        // "Dict":"Key1":"Val1","Key2":"Val2"

        var d2 = JsonConvert.DeserializeObject<Data>(newJson);
        Assert.AreEqual("Val1", d2.Dict["Key1"]);
        Assert.AreEqual("Val2", d2.Dict["Key2"]);

        var d3 = JsonConvert.DeserializeObject<Data>(oldJson);
        // Newtonsoft.Json.JsonSerializationException: Cannot deserialize the current JSON array (e.g. [1,2,3]) into 
        // type 'System.Collections.Generic.IDictionary`2[System.String,System.String]' because the type requires a JSON 
        // object (e.g. "name":"value") to deserialize correctly.
        //
        // To fix this error either change the JSON to a JSON object (e.g. "name":"value") or change the deserialized type
        // to an array or a type that implements a collection interface (e.g. ICollection, IList) like List<T> that can be 
        // deserialized from a JSON array. JsonArrayAttribute can also be added to the type to force it to deserialize from
        // a JSON array.
        //
        // Path 'Dict', line 1, position 9.

        Assert.AreEqual("Val1", d3.Dict["Key1"]);
        Assert.AreEqual("Val2", d3.Dict["Key2"]);
    

【问题讨论】:

【参考方案1】:

扩展Andrew Whitaker's answer,这是一个完全通用的版本,适用于任何类型的可写字典:

public class JsonGenericDictionaryOrArrayConverter: JsonConverter

    public override bool CanConvert(Type objectType)
    
        return objectType.GetDictionaryKeyValueTypes().Count() == 1;
    

    public override bool CanWrite  get  return false;  

    object ReadJsonGeneric<TKey, TValue>(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    
        var tokenType = reader.TokenType;

        var dict = existingValue as IDictionary<TKey, TValue>;
        if (dict == null)
        
            var contract = serializer.ContractResolver.ResolveContract(objectType);
            dict = (IDictionary<TKey, TValue>)contract.DefaultCreator();
        

        if (tokenType == JsonToken.StartArray)
        
            var pairs = new JsonSerializer().Deserialize<KeyValuePair<TKey, TValue>[]>(reader);
            if (pairs == null)
                return existingValue;
            foreach (var pair in pairs)
                dict.Add(pair);
        
        else if (tokenType == JsonToken.StartObject)
        
            // Using "Populate()" avoids infinite recursion.
            // https://github.com/JamesNK/Newtonsoft.Json/blob/ee170dc5510bb3ffd35fc1b0d986f34e33c51ab9/Src/Newtonsoft.Json/Converters/CustomCreationConverter.cs
            serializer.Populate(reader, dict);
        
        return dict;
    

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    
        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.Key, keyValueTypes.Value );
        return genericMethod.Invoke(this, new object []  reader, objectType, existingValue, serializer  );
    

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    
        throw new NotImplementedException();
    


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 settings = new JsonSerializerSettings  Converters = new JsonConverter[] new JsonGenericDictionaryOrArrayConverter()  ;

        var d2 = JsonConvert.DeserializeObject<Data>(newJson, settings);
        var d3 = JsonConvert.DeserializeObject<Data>(oldJson, settings);

【讨论】:

谢谢!那非常漂亮!我想借此机会鼓励您尝试将其引入 Newtonsoft.Json,因为我认为 javascriptserializer/数据合约序列化程序和 Newtonsoft.Json 之间的一般兼容性会很棒!【参考方案2】:

您可以为此使用自定义转换器,具体取决于字典以什么标记开头,将其反序列化为 JSON.NET 的默认方式,或将其反序列化为数组,然后将该数组转换为 Dictionary

public class DictionaryConverter : JsonConverter

    public override object ReadJson(
        JsonReader reader,
        Type objectType,
        object existingValue,
        JsonSerializer serializer)
    
        IDictionary<string, string> result;

        if (reader.TokenType == JsonToken.StartArray)
        
            JArray legacyArray = (JArray)JArray.ReadFrom(reader);

            result = legacyArray.ToDictionary(
                el => el["Key"].ToString(),
                el => el["Value"].ToString());
        
        else 
        
            result = 
                (IDictionary<string, string>)
                    serializer.Deserialize(reader, typeof(IDictionary<string, string>));
        

        return result;
    

    public override void WriteJson(
        JsonWriter writer, object value, JsonSerializer serializer)
    
        throw new NotImplementedException();
    

    public override bool CanConvert(Type objectType)
    
        return typeof(IDictionary<string, string>).IsAssignableFrom(objectType);
    

    public override bool CanWrite 
     
        get  return false;  
    

然后,您可以使用JsonConverter 属性来装饰Data 类中的Dict 属性:

public sealed class Data

    [JsonConverter(typeof(DictionaryConverter))]
    public IDictionary<string, string> Dict  get; set; 

然后反序列化两个字符串应该按预期工作。

【讨论】:

谢谢,效果很好! (我尝试了一个不起作用的 TypeConverter,但这确实有效:)【参考方案3】:

在考虑类型转换(例如,Enum 与 IComparable 的 IDictionary)的同时进一步扩展此功能,包括具有隐式运算符的类型,您可以参考我的实现,该实现缓存跨请求的类型解析。

//------------------------ JSON 转换器 --------- ----------

/// <summary>Deserializes dictionaries.</summary>
public class DictionaryConverter : JsonConverter

    private static readonly System.Collections.Concurrent.ConcurrentDictionary<Type, Tuple<Type, Type>> resolvedTypes = new System.Collections.Concurrent.ConcurrentDictionary<Type, Tuple<Type, Type>>();

    /// <summary>If this converter is able to handle a given conversion.</summary>
    /// <param name="objectType">The type to be handled.</param>
    /// <returns>Returns if this converter is able to handle a given conversion.</returns>
    public override bool CanConvert(Type objectType)
    
        if (resolvedTypes.ContainsKey(objectType)) return true;

        var result = typeof(IDictionary).IsAssignableFrom(objectType) || objectType.IsOfType(typeof(IDictionary));

        if (result) //check key is string or enum because it comes from Jvascript object which forces the key to be a string
        
            if (objectType.IsGenericType && objectType.GetGenericArguments()[0] != typeof(string) && !objectType.GetGenericArguments()[0].IsEnum)
                result = false;
        

        return result;
    

    /// <summary>Converts from serialized to object.</summary>
    /// <param name="reader">The reader.</param>
    /// <param name="objectType">The destination type.</param>
    /// <param name="existingValue">The existing value.</param>
    /// <param name="serializer">The serializer.</param>
    /// <returns>Returns the deserialized instance as per the actual target type.</returns>
    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    
        Type keyType = null;
        Type valueType = null;

        if (resolvedTypes.ContainsKey(objectType))
        
            keyType = resolvedTypes[objectType].Item1;
            valueType = resolvedTypes[objectType].Item2;
        
        else
        
            //dictionary type
            var dictionaryTypes = objectType.GetInterfaces()
                                            .Where(z => z == typeof(IDictionary) || z == typeof(IDictionary<,>))
                                            .ToList();

            if (objectType.IsInterface)
                dictionaryTypes.Add(objectType);
            else
                dictionaryTypes.Insert(0, objectType);

            var dictionaryType = dictionaryTypes.Count == 1
                                 ? dictionaryTypes[0]
                                 : dictionaryTypes.Where(z => z.IsGenericTypeDefinition)
                                                  .FirstOrDefault();

            if (dictionaryType == null) dictionaryTypes.First();

            keyType = !dictionaryType.IsGenericType
                          ? typeof(object)
                          : dictionaryType.GetGenericArguments()[0];

            valueType = !dictionaryType.IsGenericType
                            ? typeof(object)
                            : dictionaryType.GetGenericArguments()[1];

            resolvedTypes[objectType] = new Tuple<Type, Type>(keyType, valueType);
        

        // Load JObject from stream
        var jObject = JObject.Load(reader);

        return jObject.Children()
                      .OfType<JProperty>()
                      .Select(z => new  Key = z.Name, Value = serializer.Deserialize(z.Value.CreateReader(), valueType) )
                      .Select(z => new
                       
                           Key = keyType.IsEnum
                                 ? System.Enum.Parse(keyType, z.Key)
                                 : z.Key,

                           Value = z.Value.Cast(valueType)
                       )
                      .ToDictionary(z => z.Key, keyType, w => w.Value, valueType);        
    

    /// <summary>Serializes an object with default settings.</summary>
    /// <param name="writer">The writer.</param>
    /// <param name="value">The value to write.</param>
    /// <param name="serializer">The serializer.</param>
    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    
        serializer.Serialize(writer, value);
    

//-------------------- 使用的扩展方法 ---------- ---

    /// <summary>
    /// Indicates if a particular object instance at some point inherits from a specific type or implements a specific interface.
    /// </summary>
    /// <param name="sourceType">The System.Type to be evaluated.</param>
    /// <param name="typeToTestFor">The System.Type to test for.</param>
    /// <returns>Returns a boolean indicating if a particular object instance at some point inherits from a specific type or implements a specific interface.</returns>
    public static bool IsOfType(this System.Type sourceType, System.Type typeToTestFor)
    
      if (baseType == null) throw new System.ArgumentNullException("baseType", "Cannot test if object IsOfType() with a null base type");

        if (targetType == null) throw new System.ArgumentNullException("targetType", "Cannot test if object IsOfType() with a null target type");

        if (object.ReferenceEquals(baseType, targetType)) return true;

        if (targetType.IsInterface)
            return baseType.GetInterfaces().Contains(targetType)
                   ? true
                   : false;

        while (baseType != null && baseType != typeof(object))
        
            baseType = baseType.BaseType;
            if (baseType == targetType)
                return true;
        

        return false;
    

    /// <summary>Casts an object to another type.</summary>
    /// <param name="obj">The object to cast.</param>
    /// <param name="type">The end type to cast to.</param>
    /// <returns>Returns the casted object.</returns>
    public static object Cast(this object obj, Type type)
    
        var dataParam = Expression.Parameter(obj == null ? typeof(object) : obj.GetType(), "data");
        var body = Expression.Block(Expression.Convert(dataParam, type));
        var run = Expression.Lambda(body, dataParam).Compile();
        return run.DynamicInvoke(obj);
    

    /// <summary>Creates a late-bound dictionary.</summary>
    /// <typeparam name="T">The type of elements.</typeparam>
    /// <param name="enumeration">The enumeration.</param>
    /// <param name="keySelector">The function that produces the key.</param>
    /// <param name="keyType">The type of key.</param>
    /// <param name="valueSelector">The function that produces the value.</param>
    /// <param name="valueType">The type of value.</param>
    /// <returns>Returns the late-bound typed dictionary.</returns>
    public static IDictionary ToDictionary<T>(this IEnumerable<T> enumeration, Func<T, object> keySelector, Type keyType, Func<T, object> valueSelector, Type valueType)
    
        if (enumeration == null) return null;

        var dictionaryClosedType = typeof(Dictionary<,>).MakeGenericType(new Type[]  keyType, valueType );
        var dictionary = dictionaryClosedType.CreateInstance() as IDictionary;

        enumeration.ForEach(z => dictionary.Add(keySelector(z), valueSelector(z)));

        return dictionary;
    

【讨论】:

以上是关于Newtonsoft Json 将字典反序列化为来自 DataContractJsonSerializer 的键/值列表的主要内容,如果未能解决你的问题,请参考以下文章

csharp 使用Newtonsoft JSON.NET将任何对象序列化/反序列化为JSON

使用 Newtonsoft 将 JSON 反序列化为 .NET 对象(或者可能是 LINQ to JSON?)

Newtonsoft 转义 JSON 字符串无法反序列化为对象

将JSON反序列化为相对类

如何让newtonsoft将yes和no反序列化为布尔值

使用 Newtonsoft JSON 反序列化十六进制值