Search code examples
c#jsonserialization

Deserialize JSON string to object which has abstract types


I am trying to deserialize a complex nested json to my object. The only thing is that in my object I use abstract types, so it must have some logic to use the correct derived class.

The types are saved in enums.

To explain it I will keep it fairly simple and do we only nest once, but for my purpose it is more nested with objects and types.

The json

{
   "screen":{
      "type":"Component",
      "footer":{
         "type":"Bar"
      },
      "header":{
         "type":"Top"
      }
   }
}

The classes

public abstract class Screen
{
    public abstract ScreenType Type { get; }
}

public enum ScreenType
{
    Component,
    b,
    c,
    d,
    e
}

public sealed class ComponentScreen : Screen
{
    public override ScreenType Type => ScreenType.Component;
    public Header? Header { get; init; }
    public Footer? Footer { get; init; }
    public bool? ShowStuff {get; init; }

}

public abstract class Header : ITyped<HeaderType>
{
    public abstract HeaderType Type { get; }
}

public enum HeaderType
{
    Top,
    b,
    c,
    d
}

public sealed class TopScreenHeader : Header
{
    public override HeaderType Type => HeaderType.Top;
    public string MyStuff { get; }
}

It isn't possible to just change all the abstract types, or writing converters, since there are multiple abstract types with times X derived objects. The JSON is also not consistent.

My current code using newtonsoft

var screen = JsonConvert.DeserializeObject<Screen>(jsonString, new JsonSerializerSettings {
    TypeNameHandling = TypeNameHandling.Objects,
    ContractResolver = new CamelCasePropertyNamesContractResolver()

}

Which doesn't work and gives errors:

Could not create an instance of type Screens.Screen. Type is an interace or abstract class and cannot be instantiated. Path 'screen', line 1, position 10.

Solution

  • Solution

    Deserializing with TypeNameHandling is only possible if the $type property is set that maps to the exact object it should deserialize to. Without a $type property it does not deserialize with typenamehandling to my nested abstract objects.

    The $type is set when serializing the json with newtonsoft and TypeNameHandling as setting. So the received JSON should be deserialized correctly.

    If this isn't possible, than there are 2 "solutions":

    • Writing code that adds the correct $types to the json
    • Writing converters

    For me writing code inplace of converters was easier and faster. I am using it for a prototype, but I wouldn't recommend it for production.

    Both are errorprone to changes made in the package. And therefore aren't a solid solution, but it is the solution for the specific constraints and goal.

    This is how it should look like with $types:

    {
       "$type": "MyPackage.Screens.Screen, MyPackage"
       "screen":{
          "$type": "MyPackage.Screens.ComponentScreen, MyPackage",
          "type":"Component",
          "footer":{
             "$type": "MyPackage.Footer.BarFooter, MyPackage",
             "type":"Bar"
          },
          "header":{
             "$type": "MyPackage.Header.TopHeader, MyPackage",
             "type":"Top"
          }
       }
    }
    
    

    In my case the "type" property is not necessary for deserializing and can be removed.

    Deserializing in code

    To deserialize in code you need to use a wrapper where the screen is defined. Because deserializing directly to screen (abstract) is not possible.

    note

    The $type should be above the type property otherwise it isn't handled correctly. I don't know why this is the case.