Reputation: 1490
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
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