11

I have the following class, that I use as a key in a dictionary:

    public class MyClass
    {
        private readonly string _property;

        public MyClass(string property)
        {
            _property = property;
        }

        public string Property
        {
            get { return _property; }
        }

        public override bool Equals(object obj)
        {
            MyClass other = obj as MyClass;
            if (other == null) return false;
            return _property == other._property;
        }

        public override int GetHashCode()
        {
            return _property.GetHashCode();
        }
    }

The test I am running is here:

    [Test]
    public void SerializeDictionaryWithCustomKeys()
    {
        IDictionary<MyClass, object> expected = new Dictionary<MyClass, object>();
        expected.Add(new MyClass("sth"), 5.2);
        JsonSerializerSettings jsonSerializerSettings = new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.All };
        string output = JsonConvert.SerializeObject(expected, Formatting.Indented, jsonSerializerSettings);
        var actual = JsonConvert.DeserializeObject<IDictionary<MyClass, object>>(output, jsonSerializerSettings);
        CollectionAssert.AreEqual(expected, actual);
    }

The test fails, because Json.Net seems to be using the ToString() method on the dictionary keys, instead of serializing them properly. The resulting json from the test above is:

{
  "$type": "System.Collections.Generic.Dictionary`2[[RiskAnalytics.UnitTests.API.TestMarketContainerSerialisation+MyClass, RiskAnalytics.UnitTests],[System.Object, mscorlib]], mscorlib",
  "RiskAnalytics.UnitTests.API.TestMarketContainerSerialisation+MyClass": 5.2
}

which is clearly wrong. How can I get it to work?

Grzenio
  • 35,875
  • 47
  • 158
  • 240
  • 1
    JSON doesn't always handle dictionaries very well, why don't you just override `.ToString()` of the `MyClass`? – T McKeown Jul 10 '14 at 16:35
  • Why such a complex dictionary key? Why not have the dictionary value inside your class, then replace the dictionary with a list? – gunr2171 Jul 10 '14 at 16:35
  • Could you specify what the expected output should look like? Because I'm pretty sure that complex attribute names are not part of JSON... – Grx70 Jul 10 '14 at 16:42
  • @TMcKeown, in the real code there are different types of keys. I could implement `ToString()` and a corresponding type converter on all of them, but I was hoping that the serializer would do this job for me... – Grzenio Jul 10 '14 at 16:43
  • @Grx70, because my type doesn't have a type coverter from string, I was expecting to get a list of key-value pairs. – Grzenio Jul 10 '14 at 16:45
  • why aren't you specifying the `[Serializable]` attribute on the class? – T McKeown Jul 10 '14 at 16:50
  • @TMcKeown Json.NET handles serializing dictionaries fairly well, and there's no need to mark a class a serializable. – mason Jul 10 '14 at 16:52
  • 1
    @mason, apparently not. – T McKeown Jul 10 '14 at 16:57
  • My experience with JSON serialization with dictionaries has not been good either. – T McKeown Jul 10 '14 at 16:58

4 Answers4

16

This should do the trick:

Serialization:

JsonConvert.SerializeObject(expected.ToArray(), Formatting.Indented, jsonSerializerSettings);

By calling expected.ToArray() you're serializing an array of KeyValuePair<MyClass, object> objects rather than the dictionary.

Deserialization:

JsonConvert.DeserializeObject<KeyValuePair<IDataKey, object>[]>(output, jsonSerializerSettings).ToDictionary(kv => kv.Key, kv => kv.Value);

Here you deserialize the array and then retrieve the dictionary with .ToDictionary(...) call.

I'm not sure if the output meets your expectations, but surely it passes the equality assertion.

Grx70
  • 10,041
  • 1
  • 40
  • 55
  • The usage of arrays for this is not ideal imo, as Json objects are the definition of a dictionary in of themselves. It only becomes a problem when the key type can't be (de-)serialized into a plain string anymore. In that case it's a shame custom key types aren't supported at all out of the box. In the OPs example this can still be achieved with a custom JsonConverter (see my answer). – Vinz Oct 06 '22 at 14:43
10

Grx70's answer is good - just adding an alternative solution here. I ran into this problem in a Web API project where I wasn't calling SerializeObject but allowing the serialization to happen automagically.

This custom JsonConverter based on Brian Rogers' answer to a similar question did the trick for me:

public class DeepDictionaryConverter : 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.WriteStartArray();
            serializer.Serialize(writer, key);
            serializer.Serialize(writer, valueEnumerator.Current);
            writer.WriteEndArray();
        }
        writer.WriteEndArray();
    }

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

In my case, I was serializing a Dictionary<MyCustomType, int> property on a class where MyCustomType had properties like Name and Id. This is the result:

...
"dictionaryProp": [
    [
      {
        "name": "MyCustomTypeInstance1.Name",
        "description": null,
        "id": null
      },
      3
    ],
    [
      {
        "name": "MyCustomTypeInstance2.Name",
        "description": null,
        "id": null
      },
      2
    ]
]
...
Community
  • 1
  • 1
Nate Barbettini
  • 51,256
  • 26
  • 134
  • 147
5

Simpler, full solution, using a custom JsonConverter

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

public class CustomDictionaryConverter<TKey, TValue> : JsonConverter
{
    public override bool CanConvert(Type objectType) => objectType == typeof(Dictionary<TKey, TValue>);

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        => serializer.Serialize(writer, ((Dictionary<TKey, TValue>)value).ToList());

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        => serializer.Deserialize<KeyValuePair<TKey, TValue>[]>(reader).ToDictionary(kv => kv.Key, kv => kv.Value);
}

Usage:

[JsonConverter(typeof(CustomDictionaryConverter<KeyType, ValueType>))]
public Dictionary<KeyType, ValueType> MyDictionary;
Gerardo Marset
  • 803
  • 1
  • 10
  • 23
  • 3
    Great, thank you. Just word of caution -- `value` can be null in `WriteJson`, so I added small check -- `if (value == null) serializer.Serialize(writer, value); else serializer.Serialize(writer, ((Dictionary)value).ToList())` – astrowalker Dec 15 '20 at 13:24
  • An issue with this approach is, it requires creating a typed instance of these converters for each possible Dictionary that can be encountered nested in a complex chain of objects – Vinz Oct 06 '22 at 14:38
2

As your class can easily be serialized and deserialized into a plain string, this can be done with a custom Json converter while keeping the object structure of the Json.

I've written a JsonConverter for this purpose to convert any Dictionary in object style without needing to use arrays or type arguments for custom key types: Json.NET converter for custom key dictionaries in object style

The gist is going over the key-value-pairs manually and forcing serialization on the key type that originates from Json object properties. The most minimalistic working example I could produce:

public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
    // Aquire reflection info & get key-value-pairs:
    Type type = value.GetType();
    bool isStringKey = type.GetGenericArguments()[0] == typeof(string);
    IEnumerable keys = (IEnumerable)type.GetProperty("Keys").GetValue(value, null);
    IEnumerable values = (IEnumerable)type.GetProperty("Values").GetValue(value, null);
    IEnumerator valueEnumerator = values.GetEnumerator();

    // Write each key-value-pair:
    StringBuilder sb = new StringBuilder();
    using (StringWriter tempWriter = new StringWriter(sb))
    {
        writer.WriteStartObject();
        foreach (object key in keys)
        {
            valueEnumerator.MoveNext();

            // convert key, force serialization of non-string keys
            string keyStr = null;
            if (isStringKey)
            {
                // Key is not a custom type and can be used directly
                keyStr = (string)key;
            }
            else
            {
                sb.Clear();
                serializer.Serialize(tempWriter, key);
                keyStr = sb.ToString();
                // Serialization can wrap the string with literals
                if (keyStr[0] == '\"' && keyStr[str.Length-1] == '\"')
                    keyStr = keyStr.Substring(1, keyStr.Length - 1);
                // TO-DO: Validate key resolves to single string, no complex structure
            }
            writer.WritePropertyName(keyStr);

            // default serialize value
            serializer.Serialize(writer, valueEnumerator.Current);
        }
        writer.WriteEndObject();
    }
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
    // Aquire reflection info & create resulting dictionary:
    Type[] dictionaryTypes = objectType.GetGenericArguments();
    bool isStringKey = dictionaryTypes[0] == typeof(string);
    IDictionary res = Activator.CreateInstance(objectType) as IDictionary;

    // Read each key-value-pair:
    object key = null;
    object value = null;

    while (reader.Read())
    {
        if (reader.TokenType == JsonToken.EndObject)
            break;

        if (reader.TokenType == JsonToken.PropertyName)
        {
            key = isStringKey ? reader.Value : serializer.Deserialize(reader, dictionaryTypes[0]);
        }
        else
        {
            value = serializer.Deserialize(reader, dictionaryTypes[1]);

            res.Add(key, value);
            key = null;
            value = null;
        }
    }

    return res;
}

With a converter like this, JSON objects can be used as dictionaries directly, as you'd expect it. In other words one can now do this:

{
  MyDict: {
    "Key1": "Value1",
    "Key2": "Value2"
    [...]
  }
}

instead of this:

{
  MyDict: [
    ["Key1", "Value1"],
    ["Key2", "Value2"]
    [...]
  ]
}

See the repository for more details.

Vinz
  • 3,030
  • 4
  • 31
  • 52