Search code examples
c#genericsjson.netgeneric-interface

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

Solution

  • 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.