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)
3
  • This SO and this should help you. Commented Oct 24, 2015 at 18:49
  • Why not just use TypeNameHandling = TypeNameHandling.Auto? It's designed for exactly this situation and records the actual .Net type used for the interface in the JSON. Commented Oct 24, 2015 at 18:52
  • @dbc: thanks for your quick answer but it still doesn't work even if i add this to my configuration Commented Oct 25, 2015 at 9:03

1 Answer 1

11

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();
    }
}
Sign up to request clarification or add additional context in comments.

10 Comments

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?
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?
@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.
@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.
@TobiasKoller - OK, I enhanced it to check for $type. As you can see it's more complex.
|

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.