8

I am using Json.NET to deserialize an object which includes a nested Dictionary with a custom (non-string) key type. Here is a sample of what I am trying to do

public interface IInterface
{
    String Name { get; set; }
}

public class AClass : IInterface
{
    public string Name { get; set; }
}

public class Container
{
    public Dictionary<IInterface, string> Map { get; set; }
    public Container()
    {
        Map = new Dictionary<IInterface, string>();
    }
}

public static void Main(string[] args)
{
    var container = new Container();
    container.Map.Add(new AClass()
    {
        Name = "Hello World"
    }, "Hello Again");

    var settings = new JsonSerializerSettings
    {
        TypeNameHandling = TypeNameHandling.Objects,
        PreserveReferencesHandling = PreserveReferencesHandling.All,
    };

    string jsonString = JsonConvert.SerializeObject(container, Formatting.Indented, settings);
    var newContainer = JsonConvert.DeserializeObject<Container>(jsonString);
}

This yields the exception message:

Could not convert string 'ConsoleApplication1.AClass' to dictionary key type 'ConsoleApplication1.IInterface'. Create a TypeConverter to convert from the string to the key type object. Please accept my apology however I cant find a way to de-serialize interface in Dictionary key.

2 Answers 2

16

The issue is that JSON dictionaries (objects) only support string keys, so Json.Net converts your complex key type to a Json string (calling ToString()) which can't then be deserialized into the complex type again. Instead, you can serialize your dictionary as a collection of key-value pairs by applying the JsonArray attribute.

See this question for details.

Sign up to request clarification or add additional context in comments.

3 Comments

Thank you very much. Previous question suggest JsonArrayAttribute however it is only available for class but for as property. Is there anyway to mark a property with an collection or dictionary attribute please.
@duongthaiha instead of making your property of type dictionary, can you create a derived class of dictionary with the attribute ad use that?
Applying the JsonArray attribute requires that you subclass Dictionary<TKey, TValue>. One alternative is to convert the dictionary to a List<KeyValuePair<TKey, TValue>> by calling ToList() on the dictionary, and serializing the list instead. Then, when deserializing, convert it back to a dictionary using ToDictionary(x => x.Key, x => x.Value).
0

If your key type can be (de-)serialized into a plain string (as is the case with your IInterface sample), this is still possible without the use of arrays. Sadly not out of the box.

So.. I wrote a custom JsonConverter to work around this and allow for custom key types in any Dictionary without needing to use other contracts for this. It supports both serialization and deserialization: Json.NET converter for custom key dictionaries in object style

The serialization behavior is very similar to the default behavior when serializing a Dictionary in Json.NET. It is only needed to force the serialization on the key type pretty much. Here is a gist of one way this can be done:

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

De-serialization is the bigger deal as it has to create and parse generic types that cannot be specified explicitly. Thankfully reflection is pretty powerful here. This is the gist:

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 you can now do this:

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

instead of this:

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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.