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.

Vinz
  • 3,030
  • 4
  • 31
  • 52
duongthaiha
  • 855
  • 6
  • 17

2 Answers2

15

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.

Community
  • 1
  • 1
ChaseMedallion
  • 20,860
  • 17
  • 88
  • 152
  • 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 Jan 28 '14 at 10:53
  • 2
    @duongthaiha instead of making your property of type dictionary, can you create a derived class of dictionary with the attribute ad use that? – ChaseMedallion Jan 28 '14 at 16:14
  • 2
    Applying the `JsonArray` attribute requires that you subclass `Dictionary`. One alternative is to convert the dictionary to a `List>` 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)`. – Nate Cook Mar 04 '18 at 21:04
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"]
    [...]
  ]
}
Vinz
  • 3,030
  • 4
  • 31
  • 52