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
Use Built-in Features When Possible: Prefer
JsonPolymorphic
andJsonDerivedType
in .NET 7+.Security Considerations: Avoid
TypeNameHandling.All
in Newtonsoft.Json without restricting types.Custom Converters for Flexibility: When using
System.Text.Json
, implement a customJsonConverter
if necessary.Keep Serialization Efficient: Avoid excessive metadata when not required.
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.