8

How can I make Json.NET serializer to serialize IDictionary<,> instance into array of objects with key/value properties? By default it serializes the value of Key into JSON object's property name.

Basically I need something like this:

[{"key":"some key","value":1},{"key":"another key","value":5}]

instead of:

{{"some key":1},{"another key":5}}

I tried to add KeyValuePairConverter to serializer settings but it has no effect. (I found this converter is ignored for type of IDictionary<> but I cannot easily change the type of my objects as they are received from other libraries, so changing from IDictionary<> to ICollection<KeyValuePair<>> is not option for me.)

Brian Rogers
  • 125,747
  • 31
  • 299
  • 300
eXavier
  • 4,821
  • 4
  • 35
  • 57

2 Answers2

6

I was able to get this converter to work.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;

public class CustomDictionaryConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return (typeof(IDictionary).IsAssignableFrom(objectType) || 
                TypeImplementsGenericInterface(objectType, typeof(IDictionary<,>)));
    }

    private static bool TypeImplementsGenericInterface(Type concreteType, Type interfaceType)
    {
        return concreteType.GetInterfaces()
               .Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == interfaceType);
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        Type type = value.GetType();
        IEnumerable keys = (IEnumerable)type.GetProperty("Keys").GetValue(value, null);
        IEnumerable values = (IEnumerable)type.GetProperty("Values").GetValue(value, null);
        IEnumerator valueEnumerator = values.GetEnumerator();

        writer.WriteStartArray();
        foreach (object key in keys)
        {
            valueEnumerator.MoveNext();

            writer.WriteStartObject();
            writer.WritePropertyName("key");
            writer.WriteValue(key);
            writer.WritePropertyName("value");
            serializer.Serialize(writer, valueEnumerator.Current);
            writer.WriteEndObject();
        }
        writer.WriteEndArray();
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }
}

Here is an example of using the converter:

IDictionary<string, int> dict = new Dictionary<string, int>();
dict.Add("some key", 1);
dict.Add("another key", 5);

string json = JsonConvert.SerializeObject(dict, new CustomDictionaryConverter());
Console.WriteLine(json);

And here is the output of the above:

[{"key":"some key","value":1},{"key":"another key","value":5}]
Brian Rogers
  • 125,747
  • 31
  • 299
  • 300
  • 1
    It would be easier (and more complete) to check for the non-generic `System.Collections.IDictionary` instead. The generic interfaces generally extends the non-generic interfaces. Also, your `TypeImplementsGenericInterface()` method could be simplified using `Any()` instead of `Where().FirstOrDefault() != null`. – Jeff Mercado Aug 22 '13 at 17:36
  • While it would be nice, `IDictionary` actually does NOT extend the non-generic `IDictionary` interface. See the [documentation](http://msdn.microsoft.com/en-us/library/s4ys34ea.aspx). However, for the sake of completeness, I've edited my answer to add support for `IDictionary` and incorporated your other suggestion about simplifying the `TypeImplementsGenericInterface()` method. Thanks! – Brian Rogers Aug 22 '13 at 20:17
  • Ah right, I forgot about that. Correction, _immutable_ generic interfaces generally extends the corresponding _immutable_ non-generic interface, unfortunately it doesn't apply here. – Jeff Mercado Aug 22 '13 at 20:25
  • 1
    for anyone else finding this useful. if you want to correctly serialise a Dictionary or similar you need replace writer.WriteValue(valueEnumerator.Current); with serializer.Serialize(writer, valueEnumerator.Current) so that the sub object will be correctly serialize – ScottGuymer Nov 25 '13 at 16:02
  • 1
    @ScottG Thanks for the feedback. Your suggestion will work even for simple types like `int` and `string`, so this is definitely the way to go. I've edited my answer accordingly. Thanks! – Brian Rogers Nov 25 '13 at 16:46
  • How can I convert a complex dictionary that is deep in a data structure? For example I have a UserInfo class and it has a variable `List lineup` and Lineup has a `Dictionary`. I need to convert UserInfo class – Seaky Lone May 27 '18 at 05:27
3

Figured out another way - you can create custom ContractResolver and set it to JsonSerializerSettings before (de)serialization. The one below is derived from built-in CamelCasePropertyNamesContractResolver to convert serialized property names to camel case but it could be derived from DefaultContractResolver if you prefer not to modify the names.

public class DictionaryFriendlyContractResolver : CamelCasePropertyNamesContractResolver
{
    protected override JsonContract CreateContract(Type objectType)
    {
        if (objectType.IsGenericType && objectType.GetGenericTypeDefinition() == typeof(IDictionary<,>))
            return new JsonArrayContract(objectType);
        if (objectType.GetInterfaces().Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IDictionary<,>)))
            return new JsonArrayContract(objectType);
        return base.CreateContract(objectType);
    }
}

Usage:

var cfg = new JsonSerializerSettings();
cfg.ContractResolver = new DictionaryFriendlyContractResolver();
string json = JsonConvert.SerializeObject(myModel, cfg);
eXavier
  • 4,821
  • 4
  • 35
  • 57
  • Though your code works for serialization, it's not able to deserialize. This answer works in both cases http://stackoverflow.com/a/25064637/2528649 – neleus Aug 23 '16 at 15:33