Search code examples
c#jsonjson.netdeserialization

Using Newtonsoft.JSON custom converters to read json with different input


I am using NewtonSoft.Json to read/write our data as json. One (very simplified) example of this is:

{
  "$type": "MyNamespace.LandingEvent, MyAssembly",
  "TimeOfLanding": "2021-04-11T15:00:00.000Z",
  "AirportName": "KLAX",
  "AirportRunway": "25L"
}

With a C# DTO class that mimicks the properties. Note that we use TypeNameHandling.

We want to change our C# class to a more complex setup:

class Airport
{
    public string Name { get; set; }
    public string Runway { get; set; }
}

class LandingEvent
{
    public DateTime TimeOfLanding { get; set; }
    public Airport Airport { get; set; }
}

which will result in, that new data will be written to JSON as:

{
  "$type": "MyNamespace.LandingEvent, MyAssembly",
  "TimeOfLanding": "2021-04-11T15:00:00.000Z",
  "Airport": {
    "Name": "KLAX",
    "Runway": "25L"
  }
}

But we still need to be able to read the old JSON data and parse into the new class structure. And this is what I currently struggle with.

I know that the way to go is probably a specialized JsonConverter. I have a couple of questions in this regard:

  1. How do I read the $type property and instantiate the right type? (my overriden CanConvert() method is fed the name of a base-class (due to the real context being more complex than this example).
  2. I only want to do custom read, if the property AirportName exsist. How do I fall-back to default deserialization, if this is not the case?

Edit: Some clarification is in order. If I create a custom JsonConverter, then CanConvert will receive the type EventBase, but the $type can actually contain either "MyNamespace.LandingEvent, MyAssembly" or "MyNamespace.TakeoffEvent, MyAssembly". Therefore I will probably need to instantiate the returned object myself based on this value. I am not sure how, though.


Solution

  • You can use a custom JsonConverter to do double duty in handling both the polymorphic event types and the varying JSON formats. Below is an example. It works by loading the data into a JObject, where it can read the $type property and instantiate the correct event type. From there, it will try to populate the event object from the JSON. If the Airport fails to deserialize, it will then attempt to read the legacy airport proprties and populate a new Airport instance from that.

    class EventConverter : JsonConverter
    {
        public override bool CanConvert(Type objectType)
        {
            return typeof(BaseEvent).IsAssignableFrom(objectType);
        }
    
        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            JObject obj = JObject.Load(reader);
    
            string type = (string)obj["$type"];
            BaseEvent baseEvent;
            if (type.Contains(nameof(TakeoffEvent)))
            {
                baseEvent = new TakeoffEvent();
            }
            else
            {
                baseEvent = new LandingEvent();
            }
            serializer.Populate(obj.CreateReader(), baseEvent);
    
            if (baseEvent.Airport == null)
            {
                baseEvent.Airport = new Airport
                {
                    Name = (string)obj["AirportName"],
                    Runway = (string)obj["AirportRunway"]
                };
            }
            return baseEvent;
        }
    
        public override bool CanWrite => false;
    
        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            throw new NotImplementedException();
        }
    }
    

    Note: this assumes your class structure actually looks like this:

    class Airport
    {
        public string Name { get; set; }
        public string Runway { get; set; }
    }
    
    class BaseEvent
    {
        public Airport Airport { get; set; }
    }
    
    class TakeoffEvent : BaseEvent
    {
        public DateTime TimeOfTakeoff { get; set; }
    }
    
    class LandingEvent : BaseEvent
    {
        public DateTime TimeOfLanding { get; set; }
    }
    

    To use the converter, add it to the Converters collection in the JsonSerializerSettings, and pass the settings to DeserializeObject():

    var settings = new JsonSerializerSettings
    {
        TypeNameHandling = TypeNameHandling.Objects,
        Converters = new List<JsonConverter> { new EventConverter() }
    };
    
    var baseEvent = JsonConvert.DeserializeObject<BaseEvent>(json, settings);
    

    Here is a working demo: https://dotnetfiddle.net/jSaq4T

    See also: Adding backward compatibility support for an older JSON structure