JSONconverter with Interface

JsonConverter with Interface

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();
}
}

Deserializing a list of interfaces with a custom JsonConverter?

In your class, you can mark your list with a JsonProperty attribute and specify your converter with the ItemConverterType parameter:

class Foo
{
public Size Size { get; set; }

[JsonProperty(ItemConverterType = typeof(MyConverter))]
public List<IShape> Shapes { get; set; }
}

Alternatively, you can pass an instance of your converter to JsonConvert.DeserializeObject, assuming you have implemented CanConvert such that it returns true when objectType == typeof(IShape). Json.Net will then apply the converter to the items of the list.

Why is JsonConverter attribute not inherited from interface?

The key part is that classes implement interfaces, they don't inherit them.

I see the following options:

  • Use a base class
  • Use JsonStringEnumConverter "globally" instead of decorating individual members:
    var options = new JsonSerializerOptions();
    options.Converters.Add(new JsonStringEnumConverter());
    JsonSerializer.Serialize(foo);

Casting interfaces for deserialization in JSON.NET

@SamualDavis provided a great solution in a related question, which I'll summarize here.

If you have to deserialize a JSON stream into a concrete class that has interface properties, you can include the concrete classes as parameters to a constructor for the class! The NewtonSoft deserializer is smart enough to figure out that it needs to use those concrete classes to deserialize the properties.

Here is an example:

public class Visit : IVisit
{
/// <summary>
/// This constructor is required for the JSON deserializer to be able
/// to identify concrete classes to use when deserializing the interface properties.
/// </summary>
public Visit(MyLocation location, Guest guest)
{
Location = location;
Guest = guest;
}
public long VisitId { get; set; }
public ILocation Location { get; set; }
public DateTime VisitDate { get; set; }
public IGuest Guest { get; set; }
}

How to deserialize generic interface to generic concrete type with Json.Net?

Your MyConverter can be written as follows:

public class MyConverter : JsonConverter
{
public override bool CanConvert(Type objectType) =>
objectType.IsGenericType && objectType.GetGenericTypeDefinition() == typeof(IInterface<>);

public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (!CanConvert(objectType)) // For safety.
throw new ArgumentException(string.Format("Invalid type {0}", objectType));
var concreteType = typeof(Implementation<>).MakeGenericType(objectType.GetGenericArguments());
return serializer.Deserialize(reader, concreteType);
}

public override bool CanWrite => false;

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

Then add it to Converters for serialization and deserialization as follows:

var settings = new JsonSerializerSettings
{
Converters = { new MyConverter() },
};
var root = JsonConvert.DeserializeObject<IInterface<Sample>>(js_object_string, settings);

And if you really cannot change the call to DeserializeObject<IInterface<Sample>>(js_object_string) at all, you can add your converter to Json.NET's global default settings for the current thread like so:

// Set up Json.NET's global default settings to include MyConverter
JsonConvert.DefaultSettings = () => new JsonSerializerSettings
{
Converters = { new MyConverter() },
};

// And then later, deserialize to IInterface<Sample> via a call that cannot be changed AT ALL:
var root = JsonConvert.DeserializeObject<IInterface<Sample>>(js_object_string);

Alternatively, you could apply MyConverter directly to IInterface<out M> like so:

[JsonConverter(typeof(MyConverter))]
public interface IInterface<out M>
{

But if you do, you must apply NoConverter from this answer to How to deserialize generic interface to generic concrete type with Json.Net? to Implementation<M> to avoid a stack overflow exception:

[JsonConverter(typeof(NoConverter))]
public class Implementation<M> : IInterface<M>
{

Notes:

  • By overriding JsonConverter.CanWrite and returning false we avoid the need to implement WriteJson().

  • In ReadJson() we determine the concrete type to deserialize by extracting the generic parameters from the incoming objectType, which is required to be IInterface<M> for some M, and constructing a concrete type Implementation<M> using the same generic parameters.

  • Json.NET supports deserialization from a parameterized constructor as described in JSON.net: how to deserialize without using the default constructor?. Since your Implementation<M> has a single parameterized constructor that meets the requirements described, it is invoked to deserialize your concrete class correctly.

  • DefaultSettings applies to all calls to JsonConvert throughout your application, from all threads, so you should determine whether modifying these settings is appropriate for your application.

  • NoConverter must be applied to Implementation<M> because, in the absence of a converter of its own, it will inherit MyConverter from IInterface<out M> which will subsequently cause a recursive call to MyConverter.ReadJson() when deserializing the concrete type, resulting in a stack overflow or circular reference exception. (You can debug this yourself to confirm.)

    For other options to generate a "default" deserialization of the concrete class without using a converter, see JSON.Net throws StackOverflowException when using [JsonConvert()] or Call default JsonSerializer in a JsonConverter for certain value type arrays. Answers that suggest constructing a default instance of the concrete type and then populating it using JsonSerializer.Populate() will not work for you because your concrete type does not have a default constructor.

Demo fiddles for DefaultSettings here, and for MyConverter + NoConverter here.

Serialize objects implementing interface with System.Text.Json

The solution is to implement a generic converter (System.Text.Json.Serialization.JsonConverter) :

public class ElementConverter : JsonConverter<IElement>
{
public override IElement Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
throw new NotImplementedException();
}

public override void Write(Utf8JsonWriter writer, IElement value, JsonSerializerOptions options)
{
if (value is ElementA)
JsonSerializer.Serialize(writer, value as ElementA, typeof(ElementA), options);
else if (value is ElementB)
JsonSerializer.Serialize(writer, value as ElementB, typeof(ElementB), options);
else
throw new ArgumentOutOfRangeException(nameof(value), $"Unknown implementation of the interface {nameof(IElement)} for the parameter {nameof(value)}. Unknown implementation: {value?.GetType().Name}");
}
}

This just needs some more work for the Read method.



Related Topics



Leave a reply



Submit