it-swarm.com.de

Newtonsoft Json Deserialize Dictionary als Schlüssel-/Werteliste von DataContractJsonSerializer

Ich habe ein Wörterbuch mit DataContractJsonSerializer zum Speichern serialisiert, das ich mit Newtonsoft.Json deserialisieren möchte.

Der DataContractJsonSerializer hat das Dictionary zu einer Liste von Schlüssel/Wert-Paaren serialisiert:

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

Gibt es irgendwelche coolen Optionen, die ich der JsonConvert.DeserializeObject<>() geben kann, die sowohl das Datenformat als auch das Format von Newtonsoft.Json unterstützen?

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

Ist das hübsche Format, das Newtonsoft.Json erstellt, und ich möchte in einer Übergangszeit sowohl das alte DataContract-Format als auch das neue Newtonsoft-Format lesen können.

Vereinfachtes Beispiel:

    //[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

Sie können dazu einen benutzerdefinierten Konverter verwenden, abhängig davon, mit welchem ​​Token das Wörterbuch beginnt, deserialisieren Sie es in der Standardmethode von JSON.NET oder deserialisieren Sie es in ein Array und wandeln Sie dieses Array in eine Dictionary um:

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

Dann können Sie die Dict-Eigenschaft in der Data-Klasse mit einem JsonConverter-Attribut dekorieren:

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

Dann sollte die Deserialisierung beider Zeichenketten wie erwartet funktionieren.

9
Andrew Whitaker

Erweitere Andrew Whitakers Antwort , hier ist eine vollständig generische Version, die für alle Arten von schreibbaren Wörterbüchern geeignet ist:

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

Dann benutze es gerne

        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

Wenn Sie dies noch weiter ausdehnen und dabei das Casting von Typen berücksichtigen (z. B. ein IDictionary von Enum vs. IComparable), einschließlich Typen mit impliziten Operatoren, können Sie auf meine Implementierung verweisen, in der die Auflösung von Typen über Anforderungen zwischengespeichert wird.

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

// -------------------- Verwendete Erweiterungsmethoden -------------------------

    /// <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