Polymorphic Serialization in System.Text.Json

Problem

Sometimes when you're serializing a C# class to JSON, you want to include polymorphic properties in the JSON output. If you're using System.Text.Json (version 4.0.1.0 or lower) to do the serialization, this won't happen automatically. Consider the following code below for an example.

public class Chart
{
    public ChartOptions Options { get; set; }
}

public abstract class ChartOptions
{
    public bool ShowLegend { get; set; }
}

public class LineChartOptions : ChartOptions
{
    public IEnumerable<Color> DefaultLineColors { get; set; }
}

In the above code, we have a class that describes a Chart and that chart has a property with some Options. The property's type is ChartOptions, which is a base class that is common to all the different types of charts. There's a LineChartOptions class that inherits from ChartOptions and includes an additional property called DefaultLineColors that defines some colors for the lines of the line chart.

Chart chart = new Chart
{
    Options = new LineChartOptions
    {
        DefaultLineColors = new [] { Color.Red, Color.Purple, Color.Blue }
    }
};
string json = JsonSerializer.Serialize(chart);

The value of json will be {"Options":{"ShowLegend":false}}. Note that it's missing the DefaultLineColors property. Why is that happening? It's because the behavior of JsonSerializer is to only serialize members of the types that are defined in the object hierarchy at compile time. In other words, the serializer looks at the Chart class and sees that the type of the Options property is ChartOptions, so it looks at the ChartOptions class's members and only sees ShowLegend, so that's the only thing that it serializes, even though the instance of the object inside of the Options property might be a subclass of ChartOptions that includes additional properties.

Solution

So how do we get it to serialize all of the properties, including those defined in subtypes? Unfortunately, there's not a great answer to that question at the time of this writing. There are active discussions and complaints about this missing functionality on GitHub. The best solution I've found thus far (aside from switching back to Newtonsoft.Json for serialization) is to use a customer JsonConverter<T>. An example of one such converter is below. Please note that deserialization has not been implemented.

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

    public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        throw new NotImplementedException();
    }

    public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
    {
        if (value is null)
        {
            writer.WriteNullValue();
            return;
        }

        writer.WriteStartObject();
        foreach (var property in value.GetType().GetProperties())
        {
            if (!property.CanRead)
                continue;
            var propertyValue = property.GetValue(value);
            writer.WritePropertyName(property.Name);
            JsonSerializer.Serialize(writer, propertyValue, options);
        }
        writer.WriteEndObject();
    }
}

Here's how to apply it.

Chart chart = new Chart
{
    Options = new LineChartOptions
    {
        DefaultLineColors = new[] { Color.Red, Color.Purple, Color.Blue }
    }
};
var options = new JsonSerializerOptions();
options.Converters.Add(new PolymorphicJsonConverter<ChartOptions>());
string json = JsonSerializer.Serialize(chart, options);
return JsonSerializer.Serialize(this, s_serializerOptions);

The PolymorphicJsonConverter<T> is a generic type and an instance of that converter must be added to the JsonSerializerOptions for every root type in your inheritance hierarchy. For example, if you want to support polymorphic serialization for class Baz that inherits from class Bar that inherits from class Foo, then you'd need to add an instance of PolymoprhicJsonConverter<Foo> to your serializer options.

Here's the correct JSON that was generated from the example above that uses PolymorphicJsonConverter<T>.

{
    "Options":{
        "DefaultLineColors":[
            {
                "R":255,
                "G":0,
                "B":0,
                "A":255,
                "IsKnownColor":true,
                "IsEmpty":false,
                "IsNamedColor":true,
                "IsSystemColor":false,
                "Name":"Red"
            },
            {
                "R":128,
                "G":0,
                "B":128,
                "A":255,
                "IsKnownColor":true,
                "IsEmpty":false,
                "IsNamedColor":true,
                "IsSystemColor":false,
                "Name":"Purple"
            },
            {
                "R":0,
                "G":0,
                "B":255,
                "A":255,
                "IsKnownColor":true,
                "IsEmpty":false,
                "IsNamedColor":true,
                "IsSystemColor":false,
                "Name":"Blue"
            }
        ],
        "ShowLegend":false
    }
}