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.