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)

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 Coder
Code 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 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 property:

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 StackOverfloweException. 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 serialize a Coder object (derived from Person):

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 Java
Code 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 Python
Code language: plaintext (plaintext)

2 thoughts on “C# – Deserialize JSON to a derived type”

  1. Works great. As far as I see the approach using the JsonConverer-property only works if your base class is abstract. Otherwise you would get recursion for serializing the base type.

    Using the options approach you can remove the converter in case you want to deserialize as the base type. Or do you have only other idea how to handle this?

    Reply
    • You’re right – using the JsonConverter attribute with a non-abstract class causes StackOverflowException (due to infinite recursion). Thanks for pointing that out. I’ll update the article and put a warning in that section.

      To answer your question: You got it. In that scenario, use the options approach instead (and be sure to remove the custom converter from the options to prevent infinite recursion).

      Reply

Leave a Comment