AbbasFaisal
AbbasFaisal

Reputation: 1490

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

I have below interface:

public interface IInterface<out M>
{
    M Message { get; }
    string Str { get; }
}

And its implementation:

public class Implementation<M> : IInterface<M>
{
    public M Message;
    public string Str;

    public Implementation(M message, string str)
    {
        Message = message;
        Str = str;
    }

    M IInterface<M>.Message => this.Message;
    string IInterface<M>.Str => this.Str;
}

Here is a sample M class:

public class Sample
{
    public int X;
}

Here is the sample JSON I pass from javascript client:

{ "Message" : { "X": 100 }, "Str" : "abc" }

Now there is some legacy/external code (that I can't change) which tries to deserialize the above JSON object using Json.Net using DeserializeObject<IInterface<Sample>>(js_object_string).

How can I write a JsonConverter for this IInterface interface that deals with its generic parameter M. Most of the solutions on internet only work with the types that are known at compile time.

I tried below code (that I don't understand fully) but the external code doesn't think the deserialized object is IInterface.

static class ReflectionHelper
{
    public static IInterface<T> Get<T>()
    {
        var x = JsonConvert.DeserializeObject<T>(str);
        IInterface<T> y = new Implementation<T>(x, "xyz");
        return y;
    }
}

class MyConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
       return (objectType == typeof(IInterface<>));
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
       var w = Newtonsoft.Json.Linq.JObject.Load(reader);
       var x = typeof(ReflectionHelper).GetMethod(nameof(ReflectionHelper.Get)).MakeGenericMethod(objectType.GetGenericArguments()[0]).Invoke(null, new object[] {  });

       return x;
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
       serializer.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; // otherwise I get a circular dependency error.
       serializer.Serialize(writer, value);
    }
}

Upvotes: 4

Views: 3752

Answers (1)

dbc
dbc

Reputation: 116741

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.

Upvotes: 4

Related Questions