Deserialize JSON to a derived type in C#

By FoxLearn 2/5/2025 8:43:12 AM   164
To deserialize JSON into a derived type, you typically embed the type name in the JSON string. During deserialization, the system checks the type name against known derived types and deserializes it to the correct type.

However, System.Text.Json does not provide built-in support for this feature.

In this article, we will walk through how to deserialize JSON into a known derived type with System.Text.Json and also show an alternative using Newtonsoft with a helper library for whitelisting types.

Add a type name property to the base class

First, let's define an abstract base class and add a property to specify the type. We’ll call this property Category and use a string to store the type name.

public abstract class Animal
{
    public string Name { get; set; }
    public abstract string Category { get; }
}

public class Dog : Animal
{
    public override string Category { get; } = nameof(Dog);
    public string Breed { get; set; }
    public bool IsTrained { get; set; }
}

Serialize a derived type

Now, let's serialize an instance of the Dog class. We’ll cast it to the base class Animal so that the serializer will serialize the Category property as well.

using System.Text.Json;

Animal animal = new Dog()
{
    Name = "Rex",
    Breed = "German Shepherd",
    IsTrained = true
};

var json = JsonSerializer.Serialize((object)animal, new JsonSerializerOptions() { WriteIndented = true });

Console.WriteLine(json);

The output will look like this:

{
  "Category": "Dog",
  "Breed": "German Shepherd",
  "IsTrained": true,
  "Name": "Rex"
}

Deserialize to a dervived type

Now, to deserialize the JSON string back into the correct derived type, we can parse the Category property and match it with known derived types. If the Category matches Dog, we deserialize to the Dog class.

using System.Text.Json;

Animal animal;

using (var jsonDoc = JsonDocument.Parse(json))
{
    switch (jsonDoc.RootElement.GetProperty("Category").GetString())
    {
        case nameof(Dog):
            animal = jsonDoc.RootElement.Deserialize<Dog>();
            break;
        default:
            throw new JsonException("'Category' didn't match known derived types");
    }
}

Console.WriteLine($"Deserialized to type {animal.GetType()}");

The output will show that it successfully deserialized to a Dog object:

Deserialized to type Dog

Custom converter with derived type name

You can also create a custom converter to handle deserialization of types derived from the Animal class. Below is a simple implementation that checks the Category and deserializes accordingly.

using System.Text.Json;
using System.Text.Json.Serialization;

public class AnimalConverter : JsonConverter<Animal>
{
    public override bool CanConvert(Type typeToConvert)
    {
        return typeof(Animal).IsAssignableFrom(typeToConvert);
    }

    public override Animal Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        using (var jsonDoc = JsonDocument.ParseValue(ref reader))
        {
            switch (jsonDoc.RootElement.GetProperty("Category").GetString())
            {
                case nameof(Dog):
                    return jsonDoc.RootElement.Deserialize<Dog>(options);
                default:
                    throw new JsonException("'Category' doesn't match a known derived type");
            }
        }
    }

    public override void Write(Utf8JsonWriter writer, Animal animal, JsonSerializerOptions options)
    {
        JsonSerializer.Serialize(writer, (object)animal, options);
    }
}

Then, apply the converter to the base class Animal:

using System.Text.Json.Serialization;

[JsonConverter(typeof(AnimalConverter))]
public abstract class Animal
{
    public string Name { get; set; }
    public abstract string Category { get; }
}

Now you can serialize and deserialize a Dog object like this:

using System.Text.Json;

Animal animal = new Dog()
{
    Name = "Bella",
    Breed = "Golden Retriever",
    IsTrained = false
};

var options = new JsonSerializerOptions() { WriteIndented = true };
var json = JsonSerializer.Serialize(animal, options);

Console.WriteLine(json);

This outputs:

{
  "Category": "Dog",
  "Breed": "Golden Retriever",
  "IsTrained": false,
  "Name": "Bella"
}

To deserialize:

var dog = JsonSerializer.Deserialize<Animal>(json, options) as Dog;

Console.WriteLine(dog.Breed);

The output confirms it successfully deserialized to a Dog object:

Golden Retriever

Derived type deserialization with Newtonsoft and JsonSubTypes

Newtonsoft provides a built-in mechanism for deserializing derived types through the TypeNameHandling setting, but this approach poses security risks. The built-in ISerializationBinder method for whitelisting types can also be somewhat unwieldy.

A better solution is to use a helper library like JsonSubTypes. This library offers custom converters and attributes that integrate seamlessly with Newtonsoft, providing more secure and efficient ways to whitelist derived types.

First, you'll need to install both Newtonsoft.Json and JsonSubTypes via NuGet. You can do this through the Package Manager Console:

Install-Package Newtonsoft.Json
Install-Package JsonSubTypes

1. Define Base and Derived Classes

Let's define an abstract base class Vehicle and two derived classes: Car and Bike. We will also use the JsonSubtypes library to specify how to deserialize these types based on a property, in this case, Category.

using JsonSubTypes;
using Newtonsoft.Json;

[JsonConverter(typeof(JsonSubtypes), "Category")]
[JsonSubtypes.KnownSubType(typeof(Car), "Car")]
[JsonSubtypes.KnownSubType(typeof(Bike), "Bike")]
public abstract class Vehicle
{
    public string Make { get; set; }
    public abstract string Category { get; }
}

public class Car : Vehicle
{
    public override string Category { get; } = "Car";
    public int Wheels { get; set; }
    public bool IsElectric { get; set; }
}

public class Bike : Vehicle
{
    public override string Category { get; } = "Bike";
    public bool HasBasket { get; set; }
}

In this example:

  • Vehicle is the abstract base class.
  • Car and Bike are derived types that provide their own implementations of the Category property.

2. Serialize a Derived Type

Now, let’s create an instance of a Car, serialize it to JSON, and observe how the Category property is included.

using Newtonsoft.Json;

Vehicle vehicle = new Car()
{
    Make = "Tesla",
    Wheels = 4,
    IsElectric = true
};

var json = JsonConvert.SerializeObject(vehicle, Formatting.Indented);

Console.WriteLine(json);

The output will look like this:

{
  "Category": "Car",
  "Make": "Tesla",
  "Wheels": 4,
  "IsElectric": true
}

3. Deserialize into the Correct Derived Type

Now, let’s deserialize the JSON back into the correct derived type using the JsonConvert.DeserializeObject method. JsonSubTypes will match the Category property and deserialize it into the appropriate Car or Bike class.

var deserializedVehicle = JsonConvert.DeserializeObject<Vehicle>(json);

if (deserializedVehicle is Car car)
{
    Console.WriteLine($"Deserialized to a Car with {car.Wheels} wheels, electric: {car.IsElectric}");
}

This will output:

Deserialized to a Car with 4 wheels, electric: True

By using JsonSubTypes, we can easily handle derived type deserialization in a secure and efficient manner. Instead of relying on the less secure and more cumbersome TypeNameHandling or ISerializationBinder, JsonSubTypes allows us to define a clean and manageable way to deserialize into derived types based on a simple Category property.