I have the following dictionary that I'd very much like to serialize using Json.Net. The dictionary contains items of the IConvertible
interface allowing me to add whatever primitive type I need to the dictionary.
var dic = new Dictionary<string, IConvertible>();
dic.Add("bool2", false);
dic.Add("int2", 235);
dic.Add("string2", "hellohello");
I have the following implementation for serializing the list using Json.net:
var settings = new JsonSerializerSettings();
settings.TypeNameHandling = TypeNameHandling.Objects;
var dicString = JsonConvert.SerializeObject(dic, Newtonsoft.Json.Formatting.Indented, settings);
This gives me the following output:
{
"$type": "System.Collections.Generic.Dictionary`2[[System.String, mscorlib],[System.IConvertible, mscorlib]], mscorlib",
"bool2": false,
"int2": 235,
"string2": "hellohello"
}
However. When trying to deserialize as such:
var dic2 = JsonConvert.DeserializeObject<Dictionary<string, IConvertible>>(dicString);
... I get the following error:
Error converting value False to type 'System.IConvertible'. Path 'bool2', line 3, position 16.
I've looked around and found the following; but setting the typeNameHandling didn't solve it. Nor can I decorate the IConvertible
value with a type name attribute seeing as it is a dictionary.
Casting interfaces for deserialization in JSON.NET
I haven't found any other information on the topic so some help would be greatly appreciated!
I also found this solution but it involves creating a ExpandableObjectConverter which isn't a very elegant solution.
You actually have several problems here:
You seem to have encountered an odd problem with Json.NET when deserializing to a target type of IConvertible
. When deserializing a convertible primitive type, it calls the system routine Convert.ChangeType()
to convert the primitive to the target type (e.g. long
to int
). And, for some reason, this system routine throws an exception when asked to convert a primitive to type IConvertible
, even though that primitive is already of that type.
You are using TypeNameHandling.Objects
to serialize your dictionary of convertible values, however this setting is only documented to work work serializing to a JSON object. Your values, however, will be serialized as JSON primitives, so the setting does not apply.
To preserve type information for a dictionary of polymorphic primitives, you need to manually wrap the values in a container object, such as the one shown in this answer to Deserialize Dictionary<string, object> with enum values in C#. (That answer does not work here, however, because of problem #1.)
Unless you write a custom serialization binder, TypeNameHandling
is insecure and vulnerable to attack gadget injection attacks such as the ones shown in TypeNameHandling caution in Newtonsoft Json and External json vulnerable because of Json.Net TypeNameHandling auto?.
You are not using the same settings to deserialize as you are to serialize.
The above problems can be resolved with the use of the following custom JsonConverter
:
public class ConvertibleDictionaryConverter : JsonConverter
{
[JsonDictionary(ItemTypeNameHandling = TypeNameHandling.Auto)]
class ConvertibleDictionaryDTO : Dictionary<string, ConvertibleWrapper>
{
public ConvertibleDictionaryDTO() : base() { }
public ConvertibleDictionaryDTO(int count) : base(count) { }
}
public override bool CanConvert(Type objectType)
{
return typeof(IDictionary<string, IConvertible>).IsAssignableFrom(objectType);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var dto = serializer.Deserialize<ConvertibleDictionaryDTO>(reader);
if (dto == null)
return null;
var dictionary = (IDictionary<string, IConvertible>)(existingValue ?? serializer.ContractResolver.ResolveContract(objectType).DefaultCreator());
foreach (var pair in dto)
dictionary.Add(pair.Key, pair.Value.ObjectValue);
return dictionary;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var dictionary = (IDictionary<string, IConvertible>)value;
var dto = new ConvertibleDictionaryDTO(dictionary.Count);
foreach (var pair in dictionary)
dto.Add(pair.Key, ConvertibleWrapper.CreateWrapper(pair.Value));
serializer.Serialize(writer, dto);
}
}
abstract class ConvertibleWrapper
{
protected ConvertibleWrapper() { }
[JsonIgnore]
public abstract IConvertible ObjectValue { get; }
public static ConvertibleWrapper CreateWrapper<T>(T value) where T : IConvertible
{
if (value == null)
return new ConvertibleWrapper<T>();
var type = value.GetType();
if (type == typeof(T))
return new ConvertibleWrapper<T>(value);
// Return actual type of subclass
return (ConvertibleWrapper)Activator.CreateInstance(typeof(ConvertibleWrapper<>).MakeGenericType(type), value);
}
}
sealed class ConvertibleWrapper<T> : ConvertibleWrapper where T : IConvertible
{
public ConvertibleWrapper() : base() { }
public ConvertibleWrapper(T value)
: base()
{
this.Value = value;
}
public override IConvertible ObjectValue { get { return Value; } }
public T Value { get; set; }
}
Then serialize and deserialize as follows:
var settings = new JsonSerializerSettings
{
Converters = { new ConvertibleDictionaryConverter() },
};
var dicString = JsonConvert.SerializeObject(dic, Newtonsoft.Json.Formatting.Indented, settings);
var dic2 = JsonConvert.DeserializeObject<Dictionary<string, IConvertible>>(dicString, settings);
Notes:
Because [JsonDictionary(ItemTypeNameHandling = TypeNameHandling.Auto)]
is applied to ConvertibleDictionaryDTO
it is not necessary to enable TypeNameHandling.Objects
globally. This reduces your security risks.
Constraining the type of object in ConvertibleWrapper<T>
to implement IConvertible
also substantially reduces security risks because attack gadgets are highly unlikely to implement IConvertible
.
However, for additional security you may still wish to write a custom serialization binder that allows only whitelisted known types.
Working sample .Net fiddle here.