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.)

0

2 Answers 2

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}]
Sign up to request clarification or add additional context in comments.

6 Comments

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.
While it would be nice, IDictionary<K,V> actually does NOT extend the non-generic IDictionary interface. See the documentation. 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!
Ah right, I forgot about that. Correction, immutable generic interfaces generally extends the corresponding immutable non-generic interface, unfortunately it doesn't apply here.
for anyone else finding this useful. if you want to correctly serialise a Dictionary<int,Object> or similar you need replace writer.WriteValue(valueEnumerator.Current); with serializer.Serialize(writer, valueEnumerator.Current) so that the sub object will be correctly serialize
@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!
|
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);

1 Comment

Though your code works for serialization, it's not able to deserialize. This answer works in both cases stackoverflow.com/a/25064637/2528649

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.