3

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.

Carsten Gehling
  • 1,218
  • 1
  • 13
  • 31

2 Answers2

3

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

Brian Rogers
  • 125,747
  • 31
  • 299
  • 300
-1

Classes change, this kind of Json strings change and will get extra features in future versions. You'll keep adjusting your declarations. With Newtonsoft, you can add custom handlers for varying class inheritance and keep using deserialize, but you'll have to maintain that code.

For dynamic Json, I find it easier to use JObject, JArray and JToken instead, to freely parse a Json string. Especially if you're only interested in some of the fields.

I can only give you an example, I think this is (a little) related to your project, but not the same part (smiley)

I use below code to decode part of a glTF 3d-object file produced by Blender in MSFS-converted format. This Json-like format consists of sections. Each Json section looks something like this,

  "asset" : {
    "extensions" : {
        "ASOBO_normal_map_convention" : {
            "tangent_space_convention" : "DirectX"
        }
    },
    "generator" : "Extended Khronos glTF Blender I/O v1.0.0",
    "version" : "2.0"
  },

.. but these sections and their fields are mostly optional and in some GLtf's they are not filled in. It is not "serializable or deserializable" to classes.

I declare some

    public JObject AssetObject;

.. filling it in from Json string sJson as follows:

        dynamic stuff = JObject.Parse(sJson);
        var pp = stuff.Children();
        Dictionary<string, bool> d = new Dictionary<string, bool>();
        foreach (JProperty jo in pp) d[jo.Name] = true; // all sections

        string cSection= "asset";
        if (!d.ContainsKey(cSection)) { LogLine(98, "Warning: BPG Json has no " + cSection + " section."); return false; }
        else
        {
            AssetObject = (JObject)stuff[cSection];
            ParseGLBAsset();
        }

Notice the use of a dynamic declaration at first, a section will land in JObject via cast. I store the various parts of the section into string properties. The parse itself takes place in ParseGLBAsset(), this function looks as follows:

    public void ParseGLBAsset()
    {
        foreach (JProperty jbp in AssetObject.Children())
            if (jbp.Name == "generator")
            { GLBGenerator = jbp.Value.ToString(); }
            else
            if (jbp.Name == "extensions")
            {
                GLBAssetExtensions = jbp.Value.ToString(); 
                LogLine(0, "Asset extensions: " + GLBAssetExtensions);
            }
            else
              if (jbp.Name == "version")
            { GLBVersion = jbp.Value.ToString(); }
        LogLine(1, "Found asset.generator=" + GLBGenerator);
        LogLine(1, "Found asset.version=" + GLBVersion);

    }
Goodies
  • 1,951
  • 21
  • 26
  • Yes you are right, the classes will probably change again over time - this is why I try to solve the backwards-compatibility in the deserializer. But your example doesn't really answer any of my two questions. – Carsten Gehling Apr 11 '21 at 15:11
  • Hi Carsten, stackoverflow is not a coding service, and you know your Json files best. For your specific challenge, you could use the method in my answer. Success with it. – Goodies Apr 12 '21 at 07:37
  • I was not asking for anyone to code it for me. But I had two specific obstacles regarding JSON.NET, where I asked for advice for things that were not obvious in their documentation. You answered with code that in no way addressed my questions. As it happens I figured it out parallel with @brian-rogers suggestion, and we came up with almost the same solution. – Carsten Gehling Apr 14 '21 at 10:58
  • Well if you want to do e.g. FSX data it will be a viable solution to work with underlying class definitions. When the data is subject to change (e.g. MSFS), or in the case you want to be able to handle a subset of the data, or old and new formats concurrently, my method using JObject and JArray allows for field-wise changes, local context-dependent decisions and definition of the class properties independent of the precise Json format you read. Your question suggested you were looking for that. My code was just an example of max freedom. – Goodies Apr 15 '21 at 07:41