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"]
[...]
]
}