Newtonsoft Json Deserialize Dictionary as Key/Value list from DataContractJsonSerializer

30,108

Solution 1

You could use a custom converter for this, depending on what token the dictionary starts with, deserialize it JSON.NET's default way, or deserialize it into an array and then turn that array into a 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; } 
    }
}

Then, you can decorate the Dict property in the Data class with a JsonConverter attribute:

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

Then deserializing both strings should work as expected.

Solution 2

Extending Andrew Whitaker's answer, here's a completely generic version that works on any type of writable dictionary:

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

Then use it like

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

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

Solution 3

Extending this even more while accounting for casting of types (for example, an IDictionary of Enum vs. IComparable), including types with implicit operators, you can refer to my implementation which caches the resolution of types across requests.

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

//-------------------- Extension methods used -------------------------

    /// <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;
    }
Share:
30,108
Jørgen Austvik
Author by

Jørgen Austvik

Updated on July 09, 2022

Comments

  • Jørgen Austvik
    Jørgen Austvik almost 2 years

    I have a dictionary serialized to storage with DataContractJsonSerializer which I would like to deserialize with Newtonsoft.Json.

    The DataContractJsonSerializer has serialized the Dictionary to a list of Key/Value pairs:

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

    Is there any cool options I can give the JsonConvert.DeserializeObject<>() that will make it support both that data format and the format from Newtonsoft.Json?

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

    Is the pretty format Newtonsoft.Json creates, and I would like to be able to read both the old DataContract format and the new Newtonsoft format in a transition period.

    Simplified example:

        //[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"]);
        }
    
  • Jørgen Austvik
    Jørgen Austvik about 9 years
    Thank you, that worked perfectly! (I experimented with a TypeConverter which didn't work, but this does:)
  • Jørgen Austvik
    Jørgen Austvik about 9 years
    Thank you! That was very pretty! I'd like to take the opportunity to encourage you to try to get that into Newtonsoft.Json, as I think general compability between javascriptserializer/data contract serializer and Newtonsoft.Json would be great!