7

I have an object which comes from the client and get deserialized from the Web Api 2 automatically.

Now I have a problem with one property of my model. This property "CurrentField" is of Type IField and there are 2 different Implementations of this interface.

This is my model (just a dummy)

public class MyTest
{
    public IField CurrentField {get;set;}
}

public interface IField{
    string Name {get;set;}
}

public Field1 : IField{
    public string Name {get;set;}
    public int MyValue {get;set;}
}

public Field2 : IField{
    public string Name {get;set;}
    public string MyStringValue {get;set;}
}

I tried to create a custom JsonConverter to find out of what type my object from the client is (Field1 or Field2) but I just don't know how.

My Converter gets called and I can see the object when I call var obj = JObject.load(reader);

but how can I find out what type it is? I can't do something like

if(obj is Field1) ...

this is the method where I should check this right?

public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
Tobias Koller
  • 2,116
  • 4
  • 26
  • 46
  • [This SO](http://stackoverflow.com/questions/5273730/checking-for-object-type-compatibility-at-runtime) and [this](http://stackoverflow.com/questions/2742276/in-c-how-do-i-check-if-a-type-is-a-subtype-or-the-type-of-an-object) should help you. – Siva Gopal Oct 24 '15 at 18:49
  • Why not just use [`TypeNameHandling = TypeNameHandling.Auto`](http://www.newtonsoft.com/json/help/html/SerializeTypeNameHandling.htm)? It's designed for exactly this situation and records the actual .Net type used for the interface in the JSON. – dbc Oct 24 '15 at 18:52
  • @dbc: thanks for your quick answer but it still doesn't work even if i add this to my configuration – Tobias Koller Oct 25 '15 at 09:03

1 Answers1

10

How to automatically select a concrete type when deserializing an interface using Json.NET

The easiest way to solve your problem is to serialize and deserialize your JSON (on both the client and server sides) with TypeNameHandling = TypeNameHandling.Auto. If you do, your JSON will include the actual type serialized for an IFIeld property, like so:

{
  "CurrentField": {
    "$type": "MyNamespace.Field2, MyAssembly",
    "Name": "name",
    "MyStringValue": "my string value"
  }
}

However, note this caution from the Newtonsoft docs:

TypeNameHandling should be used with caution when your application deserializes JSON from an external source. Incoming types should be validated with a custom SerializationBinder when deserializing with a value other than None.

For a discussion of why this may be necessary, see TypeNameHandling caution in Newtonsoft Json, How to configure Json.NET to create a vulnerable web API, and Alvaro Muñoz & Oleksandr Mirosh's blackhat paper https://www.blackhat.com/docs/us-17/thursday/us-17-Munoz-Friday-The-13th-JSON-Attacks-wp.pdf

If for whatever reason you cannot change what the server outputs, you can create a JsonConverter that loads the JSON into a JObject and checks to see what fields are actually present, then searches through possible concrete types to find one with the same properties:

public class JsonDerivedTypeConverer<T> : JsonConverter
{
    public JsonDerivedTypeConverer() { }

    public JsonDerivedTypeConverer(params Type[] types)
    {
        this.DerivedTypes = types;
    }

    readonly HashSet<Type> derivedTypes = new HashSet<Type>();

    public IEnumerable<Type> DerivedTypes
    {
        get
        {
            return derivedTypes.ToArray(); 
        }
        set
        {
            if (value == null)
                throw new ArgumentNullException();
            derivedTypes.Clear();
            if (value != null)
                derivedTypes.UnionWith(value);
        }
    }

    JsonObjectContract FindContract(JObject obj, JsonSerializer serializer)
    {
        List<JsonObjectContract> bestContracts = new List<JsonObjectContract>();
        foreach (var type in derivedTypes)
        {
            if (type.IsAbstract)
                continue;
            var contract = serializer.ContractResolver.ResolveContract(type) as JsonObjectContract;
            if (contract == null)
                continue;
            if (obj.Properties().Select(p => p.Name).Any(n => contract.Properties.GetClosestMatchProperty(n) == null))
                continue;
            if (bestContracts.Count == 0 || bestContracts[0].Properties.Count > contract.Properties.Count)
            {
                bestContracts.Clear();
                bestContracts.Add(contract);
            }
            else if (contract.Properties.Count == bestContracts[0].Properties.Count)
            {
                bestContracts.Add(contract);
            }
        }
        return bestContracts.Single();
    }

    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(T);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        if (reader.TokenType == JsonToken.Null)
            return null;
        var obj = JObject.Load(reader); // Throws an exception if the current token is not an object.
        var contract = FindContract(obj, serializer);
        if (contract == null)
            throw new JsonSerializationException("no contract found for " + obj.ToString());
        if (existingValue == null || !contract.UnderlyingType.IsAssignableFrom(existingValue.GetType()))
            existingValue = contract.DefaultCreator();
        using (var sr = obj.CreateReader())
        {
            serializer.Populate(sr, existingValue);
        }
        return existingValue;
    }

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

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

Then you can apply that as a converter to IField:

[JsonConverter(typeof(JsonDerivedTypeConverer<IField>), new object [] { new Type [] { typeof(Field1), typeof(Field2) } })]
public interface IField
{
    string Name { get; set; }
}

Note that this solution is a little fragile. If the server omits the MyStringValue or MyValue fields (because they have default value and DefaultValueHandling = DefaultValueHandling.Ignore, for example) then the converter won't know which type to create and will throw an exception. Similarly, if two concrete types implementing IField have the same property names, differing only in type, the converter will throw an exception. Using TypeNameHandling.Auto avoids these potential problems.

Update

The following version checks to see if the "$type" parameter is present, and if TypeNameHandling != TypeNameHandling.None, falls back on default serialization. It has to do a couple of tricks to prevent infinite recursion when falling back:

public class JsonDerivedTypeConverer<T> : JsonConverter
{
    public JsonDerivedTypeConverer() { }

    public JsonDerivedTypeConverer(params Type[] types)
    {
        this.DerivedTypes = types;
    }

    readonly HashSet<Type> derivedTypes = new HashSet<Type>();

    public IEnumerable<Type> DerivedTypes
    {
        get
        {
            return derivedTypes.ToArray(); 
        }
        set
        {
            derivedTypes.Clear();
            if (value != null)
                derivedTypes.UnionWith(value);
        }
    }

    JsonObjectContract FindContract(JObject obj, JsonSerializer serializer)
    {
        List<JsonObjectContract> bestContracts = new List<JsonObjectContract>();
        foreach (var type in derivedTypes)
        {
            if (type.IsAbstract)
                continue;
            var contract = serializer.ContractResolver.ResolveContract(type) as JsonObjectContract;
            if (contract == null)
                continue;
            if (obj.Properties().Select(p => p.Name).Where(n => n != "$type").Any(n => contract.Properties.GetClosestMatchProperty(n) == null))
                continue;
            if (bestContracts.Count == 0 || bestContracts[0].Properties.Count > contract.Properties.Count)
            {
                bestContracts.Clear();
                bestContracts.Add(contract);
            }
            else if (contract.Properties.Count == bestContracts[0].Properties.Count)
            {
                bestContracts.Add(contract);
            }
        }
        return bestContracts.Single();
    }

    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(T);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        if (reader.TokenType == JsonToken.Null)
            return null;
        var obj = JObject.Load(reader); // Throws an exception if the current token is not an object.
        if (obj["$type"] != null && serializer.TypeNameHandling != TypeNameHandling.None)
        {
            // Prevent infinite recursion when using an explicit converter in the list.
            var removed = serializer.Converters.Remove(this);
            try
            {
                // Kludge to prevent infinite recursion when using JsonConverterAttribute on the type: deserialize to object.
                return obj.ToObject(typeof(object), serializer);
            }
            finally
            {
                if (removed)
                    serializer.Converters.Add(this);
            }
        }
        else
        {
            var contract = FindContract(obj, serializer);
            if (contract == null)
                throw new JsonSerializationException("no contract found for " + obj.ToString());
            if (existingValue == null || !contract.UnderlyingType.IsAssignableFrom(existingValue.GetType()))
                existingValue = contract.DefaultCreator();
            using (var sr = obj.CreateReader())
            {
                serializer.Populate(sr, existingValue);
            }
            return existingValue;
        }
    }

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

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }
}
dbc
  • 104,963
  • 20
  • 228
  • 340
  • oh man, thank you so much!! I copied your Code to VS and it works out of the box (i just had to replace the dummy classes with my real model-classes). But why is it so complicated to achive this behaviour? why do I have to create such a (nice) JsonConverter to convert an interface? – Tobias Koller Oct 25 '15 at 09:10
  • i couldn't edit my comment...first I want to thank you for your quick and almost perfect answer. It works perfect when the client-data doesn't include the $type-property. but when it includes the $type-property this bestContracts.Single(); throws an exception because it has no elements. for every type in the loop it runs in this case if (obj.Properties().Select(p => p.Name).Any(n => contract.Properties.GetClosestMatchProperty(n) == null)) continue; do you know how to tell him that the $type-property should be ignored? – Tobias Koller Oct 25 '15 at 09:22
  • 1
    @TobiasKoller - it's a little complicated because I wrote it to be general. It would look simpler if hardcoded. If the `$type` property is present then you shouldn't need any converter, Json.NET will create the correct type -- assuming you have set `TypeNameHandling = TypeNameHandling.Auto` **in both the client and server settings**. – dbc Oct 25 '15 at 09:25
  • @TobiasKoller - in other words, I'd suggest using one solution or the other, but not both. Combining them makes both more complicated. – dbc Oct 25 '15 at 09:28
  • the problem I have is that my client is an angular-web-app and I guess there is no support for TypeNameHandling (I will check if there is). sorry but which two solutions do you mean? JsonConverter or TypeNameHandling.Auto? – Tobias Koller Oct 25 '15 at 10:07
  • one more question: how can I "not use" the JsonConverter if the $type-property is set? should I check this inside the JsonConverter or are there other ways? – Tobias Koller Oct 25 '15 at 10:08
  • 1
    @TobiasKoller - falling back to the default behavior once you've entered the converter is not so easy to do, the converter just gets invoked again recursively causing a stack overflow so some kludge is required. That's why I suggest not implementing both solutions. – dbc Oct 25 '15 at 10:15
  • ok i disabled TypeNameHandling.Auto and now it works... I really appreciate your help for that problem. you saved me days of trying. I was trying and searching google for 2 days without luck. thank you!! – Tobias Koller Oct 25 '15 at 10:15
  • 1
    @TobiasKoller - OK, I enhanced it to check for `$type`. As you can see it's more complex. – dbc Oct 25 '15 at 10:55
  • maybe you should add this return (bestContracts.Count()>0)? bestContracts.Single() : null; because this threw me once an exception. But anyway - very nice code. thanks again – Tobias Koller Oct 30 '15 at 15:31