4

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);
    }
}
dbc
  • 104,963
  • 20
  • 228
  • 340
AbbasFaisal
  • 1,428
  • 2
  • 18
  • 21
  • 1
    Do you always want to deserialize `IInterface` as `Implementation`? – dbc Jan 17 '21 at 18:24
  • Yes. Because `IInterface` only has one implementation called `Implementation` and I am certainly sure it will stay that way. Regarding the question why do I need an interface then, that's a long long story that will take it out of scope of this question. – AbbasFaisal Jan 17 '21 at 18:31
  • OK. I updated my answer with a couple different ways to apply `MyConverter` if you cannot modify the call to `DeserializeObject>(js_object_string)` and pass it in directly. – dbc Jan 17 '21 at 19:40

1 Answers1

4

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.

dbc
  • 104,963
  • 20
  • 228
  • 340
  • 1
    Thanks for adding such a descriptive answer. I checked and it seems to work on first go. I will mark this the correct answer once I am fully through other things that use this code. A few questions though, 1. why do I get this circular dependency because of which I have to apply NoConverter. 2. Can't we have something better than having NoConverter as that just doesn't look elegant to me? 3. What is WriteJson and what is it used for? – AbbasFaisal Jan 17 '21 at 20:26
  • 1
    @AbbasFaisal - 1) Because `Implementation` will inherit the converter of `IInterface` unless it has one of its own to supersede it, causing a recursive call to `MyConverter.ReadJson()`. You can debug it yourself to verify. 2) This is mainly a matter of opinion. The other common solution is to construct the concrete object via its default constructor and [populate](https://www.newtonsoft.com/json/help/html/M_Newtonsoft_Json_JsonSerializer_Populate.htm) it, but your `Implementation` requires a parameterized constructor. – dbc Jan 17 '21 at 20:34
  • 1
    3) From the [docs](https://www.newtonsoft.com/json/help/html/M_Newtonsoft_Json_JsonConverter_WriteJson.htm): *Writes the JSON representation of the object.*. Since you just want a default JSON representation written, returning `false` from `CanWrite` causes Json.NET to skip calling `WriteJson()`. – dbc Jan 17 '21 at 20:34
  • Just one more question about adding NoConverter. If we don't add NoConverter, wouldn't the ReadJson when called for `Implementation` throw ArgumentException on `if (CanConvert)`, the same thing that NoConverter would do inside ReadJson. Why then do we still need NoConverter. I actually tried it without NoConverter but doesn't work, I don't get why. – AbbasFaisal Jan 19 '21 at 12:33
  • 1
    @AbbasFaisal - Calling `CanConvert(objectType)` from within `ReadJson()` does cause an argument exception to be thrown instead of a stack overflow exception, see https://dotnetfiddle.net/NE2v5K. But then **deserialization will abort because of the exception** so it won't work. Also, when a converter is applied via attributes e.g. `[JsonConverter(typeof(MyConverter))]`, then Json.NET assumes it to be able to convert the type (and its inherited types) and `CanConvert()` is not called. – dbc Jan 19 '21 at 13:46