SYL
SYL

Reputation: 155

Using JSON.NET converter to deserialize Dictionary with interface value

Given these class definitions:

public class TypeConverter<T> : JsonConverter
{
    public override bool CanConvert(Type objectType) => true;

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) => serializer.Serialize(writer, value);

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) => serializer.Deserialize<T>(reader);
}

public interface ISubStuff
{
    string Item { get; set; }
}

public class SubStuff : ISubStuff
{
   public string Item { get; set; }
}

public interface IMainStuff
{
    Dictionary<string, ISubStuff> SubStuff { get; set; }
}

I am trying to use the TypeConverter class in method declaration for deserialization as below but it's NOT working:

public class MainStuff : IMainStuff
{ 
    [JsonConverter(typeof(TypeConverter<Dictionary<string, SubStuff>>))] 
    public Dictionary<string, ISubStuff> SubStuff
    { 
        get; 
        set; 
    } 
}

The call below to deserialize the json causes an unable to cast object of type Dictionary<string, SubStuff> to Dictionary<string, ISubStuff> exception.

var jsonText = "{ \"SubStuff\": { } }"; 
var deser = JsonConvert.DeserializeObject<MainStuff><jsonText);

Upvotes: 1

Views: 2040

Answers (1)

dbc
dbc

Reputation: 117026

Your problem is that the c# Dictionary<TKey, TValue> is not covariant. I.e., even though SubStuff is an ISubStuff, Dictionary<string, SubStuff> is not a Dictionary<string, ISubStuff>. Thus when Json.NET tries to set the Dictionary<string, SubStuff> back into the MainStuff.SubStuff property, an InvalidCastException is thrown.

For a general explanation of why read/write collections are not covariant, see this answer. Its discussion of the lack of covariance of List<T> applies equally to generic dictionaries.

What you can do is use JsonProperty.ItemConverterType to apply your TypeConverter<T> only to the values in the dictionary, like so:

public class MainStuff : IMainStuff
{ 
    [JsonProperty(ItemConverterType = typeof(TypeConverter<SubStuff>))]
    public Dictionary<string, ISubStuff> SubStuff
    { 
        get; 
        set; 
    } 
}

public class TypeConverter<T> : JsonConverter
{
    public override bool CanConvert(Type objectType) 
    {
        var msg = string.Format("This converter should be applied directly with [JsonProperty(ItemConverterType = typeof(TypeConverter<{0}>))] or [JsonProperty(typeof(TypeConverter<{0}>))]", 
                                typeof(T));
        throw new NotImplementedException(msg);
    }

    public override bool CanWrite { get { return false; } }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        return serializer.Deserialize<T>(reader);
    }
}

Sample fiddle.

Incidentally, rather than inheriting from JsonConverter, you could inherit from CustomCreationConverter<> instead:

public class MainStuff : IMainStuff
{ 
    [JsonProperty(ItemConverterType = typeof(TypeConverter<ISubStuff, SubStuff>))]
    public Dictionary<string, ISubStuff> SubStuff
    { 
        get; 
        set; 
    } 
}

public class TypeConverter<T, TSerialized> : CustomCreationConverter<T> 
    where TSerialized : T, new()
{
    public override T Create(Type objectType)
    {
        return new TSerialized();
    }
}

Sample fiddle #2.

Finally, as an alternative, you could investigate use of the TypeNameHandling setting.

Upvotes: 5

Related Questions