I'm using JSON.NET to deserialize JSON (which I don't control) that represents an Event
of a specific type with inner data
:
{
"id": "abc",
"type": "a",
"data": {
// Data specific to "type"="a".
"a": 1
}
}
and
{
"id": "def",
"type": "b",
"data": {
// Data specific to "type"="b".
"b": 1
}
}
This JSON should be deserialized to the following classes:
public class Event
{
public string Id { get; }
public string Type { get; }
public EventDataBase Data { get; }
public Event(string id, string type, EventDataBase data)
{
this.Id = id;
this.Type = type;
this.Data = data;
}
}
public abstract class EventDataBase
{
}
public class AEventData : EventDataBase
{
public string A { get; }
public AEventData(string a)
{
this.A = a;
}
}
public class BEventData : EventDataBase
{
public string B { get; }
public BEventData(string b)
{
this.B = b;
}
}
The correct inherited class of EventDataBase
should be instantiated when deserializing the Event
.
There are many solutions to this problem, but they usually involve having the type
belong on the data
, not the parent. And if it does belong on the parent, the answer is to usually convert the JSON for Event
as a JObject
and then manually deserialize it. While this is possible, it feels like a hack to leverage only part of JSON.NET to do the deserialization.
The solution I've come up uses IReferenceResolver
to add a reference to the type
, and then when attempting to deserialize the data
, get the reference to the parent type, and use it to determine the concrete class that needs to be deserialized:
public class CustomContractResolver : Newtonsoft.Json.Serialization.DefaultContractResolver
{
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
var property = base.CreateProperty(member, memberSerialization);
if (member.DeclaringType == typeof(Event) && member.Name == nameof(Event.Type))
{
property.Order = Int32.MinValue;
property.MemberConverter = new EventTypeConverter();
}
else if (member.DeclaringType == typeof(Event) && member.Name == nameof(Event.Data))
{
property.Order = Int32.MaxValue;
property.MemberConverter = new EventDataConverter();
}
return property;
}
private class EventTypeConverter : JsonConverter
{
public override bool CanWrite => false;
public override bool CanConvert(Type objectType)
{
return objectType == typeof(string);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var eventType = reader.Value as string;
serializer.ReferenceResolver.AddReference(serializer, "parentEvent.type", eventType);
return eventType;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
private class EventDataConverter : JsonConverter
{
public override bool CanWrite => false;
public override bool CanConvert(Type objectType)
{
return objectType == typeof(EventDataBase);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var eventType = serializer.ReferenceResolver.ResolveReference(serializer, "parentEvent.type") as string;
var eventDataType = GetEventDataTypeFromEventType(eventType);
return serializer.Deserialize(reader, eventDataType);
}
private static Type GetEventDataTypeFromEventType(string eventType)
{
switch (eventType?.ToLower())
{
case "a":
return typeof(AEventData);
case "b":
return typeof(BEventData);
default:
throw new InvalidOperationException();
}
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
}
Here's a .NET Fiddle sample of this in action.
Is this valid use of the IReferenceResolver
? Does JSON.NET clear the reference resolver during each call deserialize (so that multiple calls don't store the old reference)? Is it thread-safe?