2

I have an API client that receives JSON serialized data from an ASP.NET Web API. Serialization and deserialization happens with JSON.Net. The serialized JSON contains type information using TypeNameHandling = TypeNameHandling.Objects, which works as expected in normal use cases.

Problem

The trouble comes from the fact that the domain models change frequently on the server side, but the consumers don't necessarily pull these updates immediately.

When this happens, the consumer requests a list of models, but serialization throws an error and the consumer can't see any returned models at all, even if only one of a list of many returned objects is unknown.

I know I can swallow the error with ErrorContext.Handled = true; but that swallows every error and I don't really want that.

Desired Behavior/Domain Example

What I want is to serialize these "unrecognized" items as specifically defined unknown items. I'd also be open to them being serialized as the base-most class (though that is abstract right now) if that's more realistic.

I want the solution to be flexible in that when a new type (new type of fruit in the domain example below) is added to the domain, it only needs to be added in once place. I don't want to have to manually register fruits in a serialization specific class.

Finally, I'd like to be able to keep visibility into other types of unhandled errors in case the pop up.

Here is an example -- bear with me on my simplified example domain:

Models

public class FruitResponse
{
   public int ResponseId {get; set;}
   public Fruit Fruit {get; set;}
}

public abstract class Fruit
{
   public string Color {get; set;}
   public decimal Weight {get; set;}
}

public class Banana : Fruit
{
   public decimal Length {get; set;}
}

public class Apple : Fruit
{
   public int WormCount {get; set;}
}

public class UnknownFruit : Fruit
{
   //whatever
}

Scenario

So now, say the client makes a request for fruit, and the server returns a list of FruitResponse objects. As long as the fruits inside those objects are apples and bananas there are no problems. However, if a new fruit type of "Orange" is added on the server side, and the $type property of the returned JSON reflects that, the request will fail.

I want the Orange (and any potential mangos, strawberries, whatever else) to get deserialized as an UnknownFruit. That way the consumer knows that there are extra fruit, sees the FruitResponse properties of those objects (FruitId), as well as the base fruit properties. As a result, new additions on the server side don't create breaking changes for the clients. Any kind of Orange specific properties coming in from the server can be ignored.

Obviously I only want this behavior for items that are fruit. They would all inherit from the Fruit base class, and also they will all be in a Fruit namespace.

I know for a fact that all new fruit will be added to the same namespace, so I'm wondering if there is a way to check for that in the $type field when deserializing.

If a Vegetable or a Foo is returned, I am ok with serialization failing.

What I've tried

I tried capturing an error inside of a custom ISerializationBinder with

    public Type BindToType(string assemblyName, string typeName)
    {
        try
        {
            return binder.BindToType(assemblyName, typeName);
        }
        catch (Exception ex)
        {
            return typeof(UnknownFruit);
        }
    }

But not only does this seem a bit hacky (using the exception for control flow), it also does not work; I get this error (ignore the odd namespacing, this is from me playing around in linqpad):

Type specified in JSON 'UserQuery+UnknownFruit, query_tykmkh, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null' is not compatible with 'UserQuery+Fruit, query_rraqao, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null'. Path 'Instrument.$type', line 1, position 124.

It also does not let me probe the $type to see if it's in the right fruit namespace.

I also know I can deserialize to an expando object, but I want to have the end result be strongly typed.

The right approach here seems to be to use a custom JsonConverter, but the approaches I've seen using that option involve manually registering models with the converter which I'd like to avoid.

I do have control over both the server and client code, so I can implement any potential change on either side.

Any help would be much appreciated.

istrupin
  • 1,423
  • 16
  • 32
  • Can you change the JSON returned by the server? Json.NET's `"$type"` functionality isn't ideal here since the `ISerializationBinder` isn't passed the expected base type. – dbc Apr 05 '19 at 18:11
  • I can -- what would you suggest? I want to make sure it binds to the right thing, because there are some naming clashes within the domain, and also all of the fruits are derived fruits, and I want them to serialize as such. – istrupin Apr 05 '19 at 18:18
  • 1
    Using custom converter can help you. Idea here in the question itself [link](https://stackoverflow.com/questions/28026386/how-can-i-deserialize-a-child-instance-as-a-parent-object-without-losing-its-spe) – SouXin Apr 05 '19 at 18:22
  • 1
    I'd suggest something like [this answer](https://stackoverflow.com/a/29531372) to [Json.Net Serialization of Type with Polymorphic Child Object](https://stackoverflow.com/q/29528648) or [this answer](https://stackoverflow.com/a/29124962) to [Polymorphic JSON Deserialization failing using Json.Net](https://stackoverflow.com/q/29124126) where you manually add in your own `"type"` property, then look up the concrete type in a custom `JsonConverter`. These mechanisms allows you to add a default case and also avoid the [security risks](https://stackoverflow.com/q/39565954) of `TypeNameHandling`. – dbc Apr 05 '19 at 18:28
  • That makes sense, but it requires me to explicitly create a mapping inside the converter for every type, correct? I'd like to avoid that if possible. Can the known type list in the converter be populated automatically via reflection or another mechanism (without too much of a performance hit)? – istrupin Apr 05 '19 at 18:46

0 Answers0