web-dev-qa-db-fra.com

Newtonsoft Json Deserialize Dictionary en tant que liste clé/valeur de DataContractJsonSerializer

J'ai un dictionnaire sérialisé au stockage avec DataContractJsonSerializer que je voudrais désérialiser avec Newtonsoft.Json.

DataContractJsonSerializer a sérialisé le dictionnaire en une liste de paires clé/valeur:

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

Y a-t-il des options intéressantes que je peux donner à la JsonConvert.DeserializeObject<>() qui lui permettront de prendre en charge à la fois ce format de données et le format de Newtonsoft.Json?

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

Est le joli format créé par Newtonsoft.Json, et j'aimerais pouvoir lire à la fois l'ancien format DataContract et le nouveau format Newtonsoft au cours d'une période de transition.

Exemple simplifié:

    //[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"]);
    }
15
Jørgen Austvik

Vous pouvez utiliser un convertisseur personnalisé pour cela, en fonction du jeton avec lequel le dictionnaire commence, désérialiser le mode par défaut de JSON.NET ou le désérialiser dans un tableau, puis transformer ce tableau en un 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; } 
    }
}

Ensuite, vous pouvez décorer la propriété Dict dans la classe Data avec un attribut JsonConverter:

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

Ensuite, la désérialisation des deux chaînes devrait fonctionner comme prévu.

9
Andrew Whitaker

En prolongeant la réponse de Andrew Whitaker , voici une version complètement générique qui fonctionne sur tout type de dictionnaire accessible en écriture:

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

Alors utilisez-le comme

        var settings = new JsonSerializerSettings { Converters = new JsonConverter[] {new JsonGenericDictionaryOrArrayConverter() } };

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

En étendant encore davantage cette opération tout en tenant compte de la transtypage de types (par exemple, un IDictionary Enum vs IComparable), y compris des types avec des opérateurs implicites, vous pouvez vous référer à mon implémentation qui met en cache la résolution des types entre les demandes.

// ---------------------- JSON Converter ------------------------ -------

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

// -------------------- Méthodes d'extension utilisées -------------------------

    /// <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;
    }
1
Miguel