C# – Deserialize JSON to a derived type

The simplest way to deserialize JSON to a derived type is to put the type name in the JSON string. Then during deserialization, match the type name property against a set of known derived types and deserialize to the target type.

System.Text.Json doesn’t have this functionality out of the box. That’s because there’s a known security flaw in JSON serializers accepting any type name from an external source. This is why it’s a good idea use a whitelist approach, where you match the type name to known derived types.

In this article, I’ll show how deserialize to a known derived type with System.Text.Json. At the end, I’ll show how to do it with Newtonsoft (and a helper library for whitelisting types).

Note: Deserializing to derived types is also known as ‘polymorphic deserialization’. Putting a property for determining which derived type you’re dealing with is also known as using a ‘type discriminator’. I’m using the wording ‘type name’ here instead. Derived type refers to subclasses and classes implementing interfaces.

Deserialize to a known derived type with System.Text.Json

I’ll show step-by-step how to deserialize JSON to a derived type with System.Text.Json. It makes sense to also show how to serialize a derived type to JSON.

Add a type name property to the base class

First, add an abstract property to the base class. You can use any name / type combo for the property. I’ve chosen to use a string called Type. Then, override it and specify a name in the subclass.

public abstract class Person
{
    public string Name { get; set; }
    public abstract string Type { get; }
}

public class Coder : Person
{
    public override string Type { get; } = nameof(Coder);
    public string Language { get; set; }
    public bool LikesJson { get; set; }
}
Code language: C# (cs)

Tip: I suggest using the nameof() operator instead of hardcoding names.

Serialize a derived type

Using JsonSerializer.Serialize(derivedType) only serializes the base class properties. To fully serialize a derived type, cast it to an object. Here’s an example:

using System.Text.Json;

Person person = new Coder()
{
    Name = "Bill",
    Language = "C#",
    LikesJson = true
};

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

Console.WriteLine(json);
Code language: C# (cs)

This outputs the following JSON. Notice that it has the Type property. This will be used in the next section to deserialize to the right derived type.

{
  "Type": "Coder",
  "Language": "C#",
  "LikesJson": true,
  "Name": "Bill"
}
Code language: JSON / JSON with Comments (json)

Deserialize to a dervived type

Parse the JSON string with JsonDocument, match the type name to a known derived type, and deserialize to the target type:

using System.Text.Json;

Person p;

using (var jsonDoc = JsonDocument.Parse(json))
{

    switch (jsonDoc.RootElement.GetProperty("Type").GetString())
    {
        case nameof(Coder):
            p = jsonDoc.RootElement.Deserialize<Coder>();
            break;
        default:
            throw new JsonException("'Type' didn't match known derived types");
    }
}

Console.WriteLine($"Deserialized to type {p.GetType()}");
Code language: C# (cs)

This outputs the following:

Deserialized to type CoderCode language: plaintext (plaintext)

The switch statement is effectively a whitelist of allowed derived types.

Custom converter with derived type name approach

You can also use this approach with a custom converter (with a few differences).

I always suggest making custom JSON converters solve your specific problem, instead of trying to make it more generic. So the following custom converter is specifically for handling (de)serialization of types derived from Person.

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

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

    public override Person Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        using (var jsonDoc = JsonDocument.ParseValue(ref reader))
        {
            //if the property isn't there, let it blow up
            switch (jsonDoc.RootElement.GetProperty("Type").GetString())
            {
                case nameof(Coder):
                    return jsonDoc.RootElement.Deserialize<Coder>(options);
                //warning: If you're not using the JsonConverter attribute approach,
                //make a copy of options without this converter
                default:
                    throw new JsonException("'Type' doesn't match a known derived type");
            }

        }
    }

    public override void Write(Utf8JsonWriter writer, Person person, JsonSerializerOptions options)
    {
        JsonSerializer.Serialize(writer, (object)person, options);
        //warning: If you're not using the JsonConverter attribute approach,
        //make a copy of options without this converter
    }
}
Code language: C# (cs)

Note: The Person and Coder classes were defined in a previous section.

I suggest applying the custom converter to the base class with the JsonConverter attribute:

using System.Text.Json.Serialization;

[JsonConverter(typeof(PersonConverter))]
public abstract class Person
{
    public string Name { get; set; }
    public abstract string Type { get; }
}

Code language: C# (cs)

Warning: Don’t use the JsonConverter attribute with a non-abstract base class, otherwise it results in a StackOverflowException. In that scenario, use the options approach instead. (Thanks to reader Kedned for pointing this out!)

This is good for two reasons:

  • Minimizes setup.
  • Don’t need to remove the custom converter from the options (I’ll explain more below).

Now create a Coder object (derived from the Person class) and serialize it to JSON:

using System.Text.Json;

Person person = new Coder()
{
    Name = "Jane",
    Language = "Java",
    LikesJson = true
};

var options = new JsonSerializerOptions() { WriteIndented = true };
var json = JsonSerializer.Serialize(person, options);
Code language: C# (cs)

This outputs the following JSON:

{
  "Type": "Coder",
  "Language": "Java",
  "LikesJson": true,
  "Name": "Jane"
}Code language: JSON / JSON with Comments (json)

Then deserialize it:

var coder = JsonSerializer.Deserialize<Person>(json, options) as Coder;

Console.WriteLine(coder.Language);
Code language: C# (cs)

This outputs, showing that it successfully deserialized to a Coder object:

Deserialized to a coder with language JavaCode language: plaintext (plaintext)

Remove custom converter from the options

If you’re adding the custom converter to JsonSerializerOptions.Converters (instead of applying it with the JsonConverter attribute), then be sure to remove the custom converter from the options in the Read() / Write() methods. Otherwise it recursively calls the Read()/Write() methods until it blows up with a StackOverflowException.

For example, make a copy of the options and remove this converter:

public override void Write(Utf8JsonWriter writer, Person person, JsonSerializerOptions options)
{ 
	var newOptions = new JsonSerializerOptions(options);
	newOptions.Converters.Remove(this);
	JsonSerializer.Serialize(writer, (object)person, newOptions);
}
Code language: C# (cs)

As an alternative to creating a copy every time, initialize a copy where you’re initializing the original JsonSerializerOptions object and pass it into the custom converter.

//Whenever you're initializing options
var optionsWithoutConverter = new JsonSerializerOptions() { WriteIndented = true };

var options = new JsonSerializerOptions(optionsWithoutConverter);
options.Converters.Add(new PersonConverter(optionsWithoutConverter));

//Use options containing the converter
var json = JsonSerializer.Serialize(person, options);
Code language: C# (cs)

As mentioned above, the best option is to apply the JsonConverter attribute. That way the custom converter isn’t part of the JsonSerializerOptions object, and this becomes a non-issue.

Derived type deserialization with Newtonsoft and JsonSubTypes

Newtonsoft has built-in functionality for deserializing derived types (using the TypeNameHandling setting), but it’s not secure, and the built-in ISerializationBinder approach for whitelisting is a bit clunky.

Use a helper library instead, like JsonSubTypes. It has custom converters and attributes that work with Newtonsoft and provides multiple ways for whitelisting derived types. I’ll show an example.

First, install the Newtonsoft and JsonSubTypes packages (View > Other Windows > Package Manager Console):

Install-Package Newtonsoft.Json
Install-Package JsonSubTypes
Code language: PowerShell (powershell)
  • Apply the JsonSubtypes custom converter, specifying which property you’ll use to determine the derived type (Type in this case).
  • Add JsonSubtypes.KnownSubType attributes for all derived types you want to support (just Coder in this case).
using JsonSubTypes;
using Newtonsoft.Json;

[JsonConverter(typeof(JsonSubtypes), "Type")]
[JsonSubtypes.KnownSubType(typeof(Coder), nameof(Coder))]
public abstract class Person
{
    public string Name { get; set; }
    public abstract string Type { get; }
}
public class Coder : Person
{
    public override string Type { get; } = nameof(Coder);
    public string Language { get; set; }
    public bool LikesJson { get; set; }
}
Code language: C# (cs)

Now serialize:

using Newtonsoft.Json;

Person person = new Coder()
{
    Name = "Tim",
    Language = "Python",
    LikesJson = true
};

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

Console.WriteLine(json);
Code language: C# (cs)

This outputs the following JSON:

{
  "Type": "Coder",
  "Language": "Python",
  "LikesJson": true,
  "Name": "Tim"
}Code language: JSON / JSON with Comments (json)

Now deserialize:

var coder = JsonConvert.DeserializeObject<Person>(json) as Coder;

Console.WriteLine($"Deserialized to a coder with language {coder.Language}");
Code language: C# (cs)

This outputs the following, showing that it successfully deserialized to a Coder object:

Deserialized to a coder with language PythonCode language: plaintext (plaintext)

Comments are closed.