Polymorphic Serialization in C#: Techniques for Handling Inheritance

Serialization is a crucial aspect of modern application development, enabling the conversion of objects into formats suitable for storage or transmission. In C#, serialization is widely used in scenarios like API communication, configuration storage, and caching. However, handling polymorphic serialization—where a base class reference may point to derived class instances—poses significant challenges.

In this article, we’ll explore different techniques for achieving polymorphic serialization in C# using System.Text.Json, Newtonsoft.Json, and other advanced approaches. We’ll also discuss best practices and potential pitfalls to avoid when dealing with polymorphic data.

Understanding Polymorphic Serialization

Polymorphic serialization occurs when objects of derived types are serialized and deserialized while maintaining their actual type information. This is particularly important in inheritance hierarchies where the base class does not explicitly define derived class properties.

Example of a Simple Inheritance Model

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

public class Dog : Animal
{
    public bool CanBark { get; set; }
}

public class Cat : Animal
{
    public bool LikesToClimb { get; set; }
}

If an Animal object is serialized without proper handling, type information is lost, resulting in only base class properties being preserved.

Polymorphic Serialization with System.Text.Json

Problem with Default Behavior

By default, System.Text.Json does not support polymorphic serialization. Consider the following:

var animals = new List<Animal>
{
    new Dog { Name = "Buddy", CanBark = true },
    new Cat { Name = "Whiskers", LikesToClimb = true }
};

var json = JsonSerializer.Serialize(animals);
Console.WriteLine(json);

This output will only contain Name properties, discarding CanBark and LikesToClimb due to the lack of explicit type handling.

Solution: Using JsonPolymorphic and JsonDerivedType Attributes (C# 11+)

Starting with .NET 7, Microsoft introduced built-in polymorphic serialization using JsonPolymorphic and JsonDerivedType attributes.

[JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")]
[JsonDerivedType(typeof(Dog), "dog")]
[JsonDerivedType(typeof(Cat), "cat")]
public abstract class Animal
{
    public string Name { get; set; }
}

Now, serialization correctly includes derived type information:

var json = JsonSerializer.Serialize(animals, new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine(json);

This results in:

[
    {
        "$type": "dog",
        "Name": "Buddy",
        "CanBark": true
    },
    {
        "$type": "cat",
        "Name": "Whiskers",
        "LikesToClimb": true
    }
]

Deserialization Handling

To deserialize, the discriminator property helps map JSON to the correct derived type:

var deserializedAnimals = JsonSerializer.Deserialize<List<Animal>>(json);

Ensure the JsonSerializerOptions used include the required attributes to support polymorphic deserialization.

Polymorphic Serialization with Newtonsoft.Json

Using Type Name Handling

Newtonsoft.Json provides an easier way to enable polymorphic serialization using TypeNameHandling.All:

var settings = new JsonSerializerSettings
{
    TypeNameHandling = TypeNameHandling.All,
    Formatting = Formatting.Indented
};

var json = JsonConvert.SerializeObject(animals, settings);
Console.WriteLine(json);

Output:

[
    {
        "$type": "Namespace.Dog, AssemblyName",
        "Name": "Buddy",
        "CanBark": true
    },
    {
        "$type": "Namespace.Cat, AssemblyName",
        "Name": "Whiskers",
        "LikesToClimb": true
    }
]

Deserialization is straightforward:

var deserializedAnimals = JsonConvert.DeserializeObject<List<Animal>>(json, settings);

Security Considerations

TypeNameHandling.All can expose security risks, as it allows deserialization of arbitrary types. A safer approach is using TypeNameHandling.Objects along with a SerializationBinder to restrict allowed types.

Custom Polymorphic Serialization with System.Text.Json

For more control, a custom JsonConverter can be implemented:

public class AnimalConverter : JsonConverter<Animal>
{
    public override Animal Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        using (JsonDocument doc = JsonDocument.ParseValue(ref reader))
        {
            var root = doc.RootElement;
            var type = root.GetProperty("$type").GetString();
            return type switch
            {
                "dog" => JsonSerializer.Deserialize<Dog>(root.GetRawText(), options),
                "cat" => JsonSerializer.Deserialize<Cat>(root.GetRawText(), options),
                _ => throw new JsonException()
            };
        }
    }

    public override void Write(Utf8JsonWriter writer, Animal value, JsonSerializerOptions options)
    {
        var type = value switch
        {
            Dog => "dog",
            Cat => "cat",
            _ => throw new JsonException()
        };

        using (JsonDocument doc = JsonDocument.Parse(JsonSerializer.Serialize(value, value.GetType(), options)))
        {
            writer.WriteStartObject();
            writer.WriteString("$type", type);
            foreach (var prop in doc.RootElement.EnumerateObject())
            {
                prop.WriteTo(writer);
            }
            writer.WriteEndObject();
        }
    }
}

Register the custom converter:

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

Best Practices for Polymorphic Serialization

  1. Use Built-in Features When Possible: Prefer JsonPolymorphic and JsonDerivedType in .NET 7+.

  2. Security Considerations: Avoid TypeNameHandling.All in Newtonsoft.Json without restricting types.

  3. Custom Converters for Flexibility: When using System.Text.Json, implement a custom JsonConverter if necessary.

  4. Keep Serialization Efficient: Avoid excessive metadata when not required.

  5. Use Schema Validation: When working with APIs, validate the JSON structure before deserialization.

Conclusion

Polymorphic serialization is a powerful feature that allows developers to handle complex inheritance scenarios efficiently. While System.Text.Json has improved significantly in .NET 7+, Newtonsoft.Json remains a strong alternative with better legacy support. Choosing the right approach depends on performance needs, security concerns, and flexibility requirements.

By implementing these best practices and using the right serialization strategy, you can implement robust and efficient handling of polymorphic objects in C# applications.