Newtonsoft Json Deserialize Dictionary как список клавиш/значений из DataContractJsonSerializer

У меня есть словарь, сериализованный для хранения с помощью DataContractJsonSerializer, который я хотел бы десериализовать с помощью Newtonsoft.Json.

DataContractJsonSerializer сериализовал словарь в список пар ключ/значение:

{"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

Для этого вы можете использовать собственный конвертер, в зависимости от того, с какого токена начинается словарь, десериализуйте его по умолчанию на 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; } 
    }
}

Затем вы можете украсить свойство Dict в классе Data атрибутом JsonConverter:

public sealed class Data
{
    [JsonConverter(typeof(DictionaryConverter))]
    public IDictionary<string, string> Dict { get; set; }
}

Затем десериализация обеих строк должна работать как ожидалось.

Ответ 2

Расширение ответа Andrew Whitaker, здесь полностью общая версия, которая работает с любым типом записываемого словаря:

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);

Ответ 3

Расширение этого даже при учете различения типов (например, IDictionary из Enum vs. IComparable), включая типы с неявными операторами, вы можете ссылаться на мою реализацию, которая кэширует разрешение типов по запросам.

//---------------------- Конвертер 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;
    }