Photo by Florian Krumm

The problem

We need to deserialize the following JSON:

[
  {
    "type": "car",
    "name": "Josefs Car",
    "wheels": 4,
    "model": {
      "brand": "Ferrri",
      "name": "458 Spider",
      "color": "red",
      "horsepower": 562
    },
    "properties": {
      "passengers": 2,
      "rims": {
        "name": "Nutek"
      }
    }
  },
  {
    "type": "truck",
    "name": "Josefs Truck",
    "wheels": 8,
    "model": {
      "brand": "Volvo",
      "name": "FH",
      "color": "black",
      "horsepower": 540
    },
    "trailer": {
      "name": "my trailer name",
      "wheels": 8
    },
    "properties": {
      "passengers": 2,
      "towingCapacity": {
        "maxKg": 18000,
        "maxPounds": 39683
      }
    }
  }
]

As you can see, the JSON differs a bit depending on what type of vehicle it is.

  • The properties of a car contains a rims property.
  • The properties of a truck contains a towingCapacity property.
  • A truck also has a trailer property.

Approaches

We will use the following custom JsonSerializerOptions for all the different approaches.

public static class DefaultJsonSerializerOptions
{
    static DefaultJsonSerializerOptions()
    {
        Instance = new JsonSerializerOptions
        {
           // Don't care about the casing
           PropertyNameCaseInsensitive = true
        };
    }

    public static JsonSerializerOptions Instance { get; }
}

All of our classes will be immutable as well.

Non polymorphic

We start by creating the classes required for our deserialization.

public class Vehicle
{
    public Vehicle(string type, string name, int wheels, Trailer trailer, VehicleModel model, VehicleProperties properties)
    {
        Type = type;
        Name = name;
        Wheels = wheels;
        Trailer = trailer;
        Model = model;
        Properties = properties;
    }

    public string Type { get; }
    public string Name { get; }
    public int Wheels { get; }
    public VehicleModel Model { get; }
    public VehicleProperties Properties { get; }
    public Trailer Trailer { get; }
}


public class VehicleModel
{
    public VehicleModel(string brand, string name, string color, int horsepower)
    {
        Brand = brand;
        Name = name;
        Color = color;
        Horsepower = horsepower;
    }

    public string Brand { get; }
    public string Name { get; }
    public string Color { get; }
    public int Horsepower { get; }
}


public class VehicleProperties
{
    public VehicleProperties(int wheels, int passengers, Rims rims, TruckTowingCapacity towingCapacity)
    {
        Wheels = wheels;
        Passengers = passengers;
        Rims = rims;
        TowingCapacity = towingCapacity;
    }

    public int Wheels { get; }
    public int Passengers { get; }
    public TruckTowingCapacity TowingCapacity { get; }
    public Rims Rims { get; }
}


public class TruckTowingCapacity
{
    public TruckTowingCapacity(int maxKg, int maxPounds)
    {
        MaxKg = maxKg;
        MaxPounds = maxPounds;
    }

    public int MaxKg { get; }
    public int MaxPounds { get; }
}


public class Rims
{
    public Rims(string name)
    {
       Name = name;
    }

    public string Name { get; }
}

Test
We then write the following test:

public class NonPolymorphicTests
{
    [Fact]
    public async Task ShouldDeserializeVehiclesCorrectly()
    {
        using (var jsonFile = File.OpenRead("example.json"))
        {
            var result = await JsonSerializer.DeserializeAsync<List<NonPolymorphic.Vehicle>>(jsonFile, DefaultJsonSerializerOptions.Instance);

            result.Count.ShouldBe(2);
            var car = result.Single(x => x.Type == "car");
            var truck = result.Single(x => x.Type == "truck");
            car.Type.ShouldBe("car");
            car.Name.ShouldBe("Josefs Car");
            car.Model.Brand.ShouldBe("Ferrari");
            car.Model.Name.ShouldBe("458 Spider");
            car.Model.Color.ShouldBe("red");
            car.Model.Horsepower.ShouldBe(562);
            car.Properties.Wheels.ShouldBe(4);
            car.Properties.Passengers.ShouldBe(2);
            car.Properties.Rims.Name.ShouldBe("Nutek");
            truck.Type.ShouldBe("truck");
            truck.Name.ShouldBe("Josefs Truck");
            truck.Model.Name.ShouldBe("FH");
            truck.Model.Brand.ShouldBe("Volvo");
            truck.Model.Color.ShouldBe("black");
            truck.Model.Horsepower.ShouldBe(540);
            truck.Properties.Wheels.ShouldBe(8);
            truck.Properties.Passengers.ShouldBe(2);
            truck.Properties.TowingCapacity.MaxKg.ShouldBe(18000);
            truck.Properties.TowingCapacity.MaxPounds.ShouldBe(39683);
            truck.Trailer.Name.ShouldBe("my trailer");
            truck.Trailer.Wheels.ShouldBe(8);
        }
    }
}

If we now run this test, it passes, great.
However...there's a few problems with this approach:

  • All objects in our list will be a Vehicle.
  • A "car" will have a trailer property.
  • A "car" will have a TowingCapacity property in it's properties.
  • A "truck" will have a Rims property.
  • We can't use pattern matching since everything is of the same type (Vehicle).
  • We can't do our null checking in the constructor because some properties NEEDS to be null now (towingCapacity on a "car" for example).

Depending on your use case this might be good enough for you, but for me, this doesn't cut it.

Polymorphic

Let's start with creating some new classes:

public abstract class Vehicle<T> : Vehicle where T : VehicleProperties
{
    protected Vehicle(string type, string name, VehicleModel model, T properties) : base(type, name, model)
    {
        Properties = properties ?? throw new ArgumentNullException(nameof(properties));
    }

    public T Properties { get; }
}

public abstract class Vehicle
{
    protected Vehicle(string type, string name, VehicleModel model)
    {
        Type = type ?? throw new ArgumentNullException(nameof(type));
        Name = name ?? throw new ArgumentNullException(nameof(name));
        Model = model ?? throw new ArgumentNullException(nameof(model));
    }

    public string Type { get; }
    public string Name { get; }
    public VehicleModel Model { get; }
}


public class Car : Vehicle<CarProperties>
{
    public Car(string type, string name, VehicleModel model, CarProperties properties) : base(type, name, model, properties)
    {
    }
}


public class Truck : Vehicle<TruckProperties>
{
    public Truck(
       string type,
        string name,
        VehicleModel model,
        TruckProperties properties,
        TruckTrailer trailer) : base(type,
        name,
        model,
        properties)
    {
        Trailer = trailer;
    }

    public TruckTrailer Trailer { get; }
}


public class VehicleModel
{
    public VehicleModel(string brand, string name, string color, int horsepower)
    {
        Brand = brand ?? throw new ArgumentNullException(nameof(brand));
        Name = name ?? throw new ArgumentNullException(nameof(name));
        Color = color ?? throw new ArgumentNullException(nameof(color));
        Horsepower = horsepower;
    }

    public string Brand { get; }
    public string Name { get; }
    public string Color { get; }
    public int Horsepower { get; }
}


public abstract class VehicleProperties
{
    protected VehicleProperties(int wheels, int passengers)
    {
        Wheels = wheels;
        Passengers = passengers;
    }

    public int Wheels { get; }
    public int Passengers { get; }
}


public class CarProperties : VehicleProperties
{
    public CarProperties(int wheels, int passengers, CarRims rims) : base(wheels, passengers)
    {
        Rims = rims ?? throw new ArgumentNullException(nameof(rims));
    }

    public CarRims Rims { get; }
}


public class CarRims
{
    public CarRims(string name)
    {
        Name = name ?? throw new ArgumentNullException(nameof(name));
    }

    public string Name { get; }
}


public class TruckProperties : VehicleProperties
{
    public TruckProperties(int wheels, int passengers, TruckTowingCapacity towingCapacity) : base(wheels, passengers)
    {
        TowingCapacity = towingCapacity ?? throw new ArgumentNullException(nameof(towingCapacity));
    }
    public TruckTowingCapacity TowingCapacity { get; }
}


public class TruckTowingCapacity
{
    public TruckTowingCapacity(int maxKg, int maxPounds)
    {
        MaxKg = maxKg;
        MaxPounds = maxPounds;
    }

    public int MaxKg { get; }
    public int MaxPounds { get; }
}


public class TruckTrailer
{
    public TruckTrailer(string name, int wheels)
    {
        Name = name ?? throw new ArgumentNullException(nameof(name));
        Wheels = wheels;
    }

    public string Name { get; }
    public int Wheels { get; }
}

We've created an abstract class, Vehicle, together with two implementations, Car and Truck. Now the car specific properties are only accessible on a Car and vice versa, great.

Test

public class PolymorphicTests
{
    [Fact]
    public async Task ShouldDeserializeVehiclesCorrectly()
    {
        using (var jsonFile = File.OpenRead("example.json"))
        {
            var result = await JsonSerializer.DeserializeAsync<List<Polymorphic.Vehicle>>(jsonFile, DefaultJsonSerializerOptions.Instance);

            result.Count.ShouldBe(2);
            result.ShouldContain(x => x.GetType() == typeof(Car));
            result.ShouldContain(x => x.GetType() == typeof(Truck));
            var car = result.Single(x => x.Type == "car") as Car;
            var truck = result.Single(x => x.Type == "truck") as Truck;
            car.Type.ShouldBe("car");
            car.Name.ShouldBe("Josefs Car");
            car.Model.Brand.ShouldBe("Ferrari");
            car.Model.Name.ShouldBe("458 Spider");
            car.Model.Color.ShouldBe("red");
            car.Model.Horsepower.ShouldBe(562);
            car.Properties.Wheels.ShouldBe(4);
            car.Properties.Passengers.ShouldBe(2);
            car.Properties.Rims.Name.ShouldBe("Nutek");
            truck.Type.ShouldBe("truck");
            truck.Name.ShouldBe("Josefs Truck");
            truck.Model.Name.ShouldBe("FH");
            truck.Model.Brand.ShouldBe("Volvo");
            truck.Model.Color.ShouldBe("black");
            truck.Model.Horsepower.ShouldBe(540);
            truck.Properties.Wheels.ShouldBe(8);
            truck.Properties.Passengers.ShouldBe(2);
            truck.Properties.TowingCapacity.MaxKg.ShouldBe(18000);
            truck.Properties.TowingCapacity.MaxPounds.ShouldBe(39683);
            truck.Trailer.Name.ShouldBe("my trailer");
            truck.Trailer.Wheels.ShouldBe(8);
        }
    }
}

If we run this test now...it will FAIL.

System.NotSupportedException
Deserialization of types without a parameterless constructor, a singular parameterized constructor, or a parameterized constructor annotated with 'JsonConstructorAttribute' is not supported. Type 'JOS.SystemTextJsonPolymorphism.Polymorphic.Vehicle'. Path: $[0] | LineNumber: 1 | BytePositionInLine: 3.
   at System.Text.Json.ThrowHelper.ThrowNotSupportedException(ReadStack& state, Utf8JsonReader& reader, NotSupportedException ex)
   at System.Text.Json.ThrowHelper.ThrowNotSupportedException_DeserializeNoConstructor(Type type, Utf8JsonReader& reader, ReadStack& state)
   at System.Text.Json.Serialization.Converters.ObjectDefaultConverter`1.OnTryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, T& value)
   at System.Text.Json.Serialization.JsonConverter`1.TryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, T& value)
   at System.Text.Json.Serialization.Converters.IEnumerableDefaultConverter`2.OnTryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, TCollection& value)
   at System.Text.Json.Serialization.JsonConverter`1.TryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, T& value)
   at System.Text.Json.Serialization.JsonConverter`1.ReadCore(Utf8JsonReader& reader, JsonSerializerOptions options, ReadStack& state)
   at System.Text.Json.JsonSerializer.ReadCore[TValue](JsonConverter jsonConverter, Utf8JsonReader& reader, JsonSerializerOptions options, ReadStack& state)
   at System.Text.Json.JsonSerializer.ReadCore[TValue](JsonReaderState& readerState, Boolean isFinalBlock, ReadOnlySpan`1 buffer, JsonSerializerOptions options, ReadStack& state, JsonConverter converterBase)
   at System.Text.Json.JsonSerializer.ReadAsync[TValue](Stream utf8Json, Type returnType, JsonSerializerOptions options, CancellationToken cancellationToken)
   at JOS.SystemTextJsonPolymorphism.Tests.PolymorphicTests.ShouldDeserializeVehiclesCorrectly()

We are getting the error because we are trying to deserialize to an abstract immutable class, we need to help the serializer a bit here by creating a custom JsonConverter.

Custom JsonConverter.

I've written about System.Text.Json and custom JsonConverters before. It's a really flexible way of handling some custom logic when serializing/deserializing.

public class VehicleJsonConverter : JsonConverter<Vehicle>
{
    public override bool CanConvert(Type type)
    {
        return type.IsAssignableFrom(typeof(Vehicle));
    }

    public override Vehicle Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        if (JsonDocument.TryParseValue(ref reader, out var doc))
        {
            if (doc.RootElement.TryGetProperty("type", out var type))
            {
                var typeValue = type.GetString();
                var rootElement = doc.RootElement.GetRawText();

                return typeValue switch
                {
                    "car" => JsonSerializer.Deserialize<Car.Car>(rootElement, options),
                    "truck" => JsonSerializer.Deserialize<Truck.Truck>(rootElement, options),
                    _ => throw new JsonException($"{typeValue} has not been mapped to a custom type yet!")
                };
            }

            throw new JsonException("Failed to extract type property, it might be missing?");
        }

        throw new JsonException("Failed to parse JsonDocument");
    }

    public override void Write(Utf8JsonWriter writer, Vehicle value, JsonSerializerOptions options)
    {
        // We don't care about writing JSON.
        throw new NotImplementedException();
    }
}

Let's break it down:

  1. We say that this converter handles everything that's assignable from a Vehicle
  2. We parse the JSON to a JsonDocument
  3. If successful, we look for a type property.
  4. If a type property exists, we switch on it and deserialize to the correct type (Car or Truck in our case).

The only thing left to do now is to tell the JsonSerializer to use our custom converter. We can do this in a couple of different ways, I'm choosing to use the attribute.

[JsonConverter(typeof(VehicleJsonConverter))]
public abstract class Vehicle
{
    protected Vehicle(string type, string name, VehicleModel model)
    {
        Type = type ?? throw new ArgumentNullException(nameof(type));
        Name = name ?? throw new ArgumentNullException(nameof(name));
        Model = model ?? throw new ArgumentNullException(nameof(model));
    }

    public string Type { get; }
    public string Name { get; }
    public VehicleModel Model { get; }
}

Now if we run the test again, it's green!

Bonus - C# 9 - Covariant return types

One problem with how we structured our Vehicle class above is that the non-generic Vehicle class doesn't have a Properties property. So this would not work for example:

var result = await JsonSerializer.DeserializeAsync<List<Polymorphic.Vehicle>>(jsonFile, DefaultJsonSerializerOptions.Instance);

result.Select(x => x.Properties);

That code will not compile because we are deserializing to the non-generic Vehicle class and it does not contain any property named Properties.

This can be fixed in a number of ways but since I like to use the latest and greatest™, I will solve it by using covariant return types that was introduced in C# 9.

We need to do some modifications to our Vechicle, Car and Truck classes.

[JsonConverter(typeof(VehicleJsonConverter))]
public abstract class Vehicle
{
    protected Vehicle(string type, string name, VehicleModel model)
    {
        Type = type ?? throw new ArgumentNullException(nameof(type));
        Name = name ?? throw new ArgumentNullException(nameof(name));
        Model = model ?? throw new ArgumentNullException(nameof(model));
    }

    public string Type { get; }
    public string Name { get; }
    public VehicleModel Model { get; }
    public abstract VehicleProperties Properties { get; }
}

We've now removed the generic Vehicle class altogether and instead added the Properties property to the non-generic Vechicle class and marked it abstract.

public class Car : Vehicle
{
    public Car(string type, string name, VehicleModel model, CarProperties properties) : base(type, name, model)
    {
        Properties = properties ?? throw new ArgumentNullException(nameof(properties));
    }

    public override CarProperties Properties { get; }
}


public class Truck : Vehicle
{
    public Truck(
        string type,
        string name,
        VehicleModel model,
        TruckProperties properties,
        TruckTrailer trailer) : base(type,
        name,
        model)
    {
        Trailer = trailer;
        Properties = properties;
    }

    public TruckTrailer Trailer { get; }
    public override TruckProperties Properties { get; }
}

We can now change the type of the Properties property by overriding it, HOW COOL?

If we now try do access the Properties property like this...

var result = await JsonSerializer.DeserializeAsync<List<Polymorphic.Vehicle>>(jsonFile, DefaultJsonSerializerOptions.Instance);

result.Select(x => x.Properties);

...it will compile and just work :).

All code in this post can be found over at Github.