web-dev-qa-db-ja.com

Newtonsoft Jsonは、DataContractJsonSerializerのキー/値リストとして辞書をデシリアライズします

Newtonsoft.Jsonで逆シリアル化したいDataContractJsonSerializerでストレージにシリアル化された辞書があります。

DataContractJsonSerializerは、ディクショナリをキーと値のペアのリストにシリアル化しました。

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

そのデータ形式とNewtonsoft.Jsonの形式の両方をサポートするJsonConvert.DeserializeObject<>()を提供できるクールなオプションはありますか?

_{"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"]);
    }
_
16
Jørgen Austvik

辞書の最初のトークンに応じて、これにカスタムコンバーターを使用したり、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; }
}

次に、両方の文字列を逆シリアル化すると、期待どおりに機能するはずです。

11
Andrew Whitaker

拡張 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);
22
dbc

暗黙の演算子を含む型を含む型のキャスト(たとえば、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;
    }
1
Miguel