2

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?

Community
  • 1
  • 1
TheCloudlessSky
  • 18,608
  • 15
  • 75
  • 116
  • One problem is that, if your JSON contains more than one `Event`, deserialization will fail due to an attempt to add duplicate keys to the reference resolver table. See https://dotnetfiddle.net/VULtfu for a demo. A second problem is that a [JSON object](http://www.json.org/) is defined by the standard to be *an unordered set of name/value pairs* so you really shouldn't be assuming the `"type"` property comes before the `"data"` property. – dbc May 11 '17 at 06:58
  • @dbc The good news is that the JSON we receive will not be in the format that I presented. So it will be impossible to have multiple events per JSON payload. Also, after posting this question, I too realized that the `type` property can come *after* the `data` property. I switched to doing a double-parse (similar to JSON.NET's metadata handling) to determine the "type" ahead of time. However, I still need to pass the "type" around to the "data" property's convert so I'm still using the solution I posted here. – TheCloudlessSky May 12 '17 at 18:16
  • I can't edit my last comment but I meant "...the JSON we receive will *only* be in the format that I presented". – TheCloudlessSky May 13 '17 at 21:13

0 Answers0