Featured image of post Polymorphic deserialization with YamlDotNet

Polymorphic deserialization with YamlDotNet

Deserialize C# type hierarchies using a dedicated type discriminator key to map YAML values to their corresponding C# types.

Suppose you have a Shape class and derived classes Rectangle, Circle, and Triangle:

public abstract class Shape;

public sealed class Rectangle : Shape
{
    [YamlMember(Alias = "width")]
    public int? Width { get; set; }

    [YamlMember(Alias = "height")]
    public int? Height { get; set; }
}

public sealed class Circle : Shape
{
    [YamlMember(Alias = "radius")]
    public int? Radius { get; set; }
}

public sealed class Triangle : Shape
{
    [YamlMember(Alias = "side1")]
    public int? Side1 { get; set; }

    [YamlMember(Alias = "side2")]
    public int? Side2 { get; set; }

    [YamlMember(Alias = "side3")]
    public int? Side3 { get; set; }
}

You may want to deserialize an arbitrary list of YAML-serialized shapes into the corresponding C# objects. To achieve this, YamlDotNet allows you to define a special YAML key as a type discriminator. The value associated with this key determines the type of object to instantiate.

Let’s create a YAML array of shapes, adding a type discriminator:

- type: rectangle
  width: 10
  height: 20

- type: circle
  radius: 5

- type: triangle
  side1: 6
  side2: 9
  side3: 5

We can build a deserializer and use the WithTypeDiscriminatingNodeDeserializer method to specify the type discriminator and the mapping of types to each discriminator value:

var deserializer = new DeserializerBuilder()
    .IgnoreUnmatchedProperties() // Prevent YamlDotNet from throwing if it doesn't find a C# property for the discriminator key.
    .WithTypeDiscriminatingNodeDeserializer(options =>
    {
        options.AddKeyValueTypeDiscriminator<Shape>("type", new Dictionary<string, Type>(StringComparer.Ordinal)
        {
            ["rectangle"] = typeof(Rectangle),
            ["circle"] = typeof(Circle),
            ["triangle"] = typeof(Triangle),
        });
    })
    .Build();

This deserializer can handle the YAML payload polymorphically as a collection of Shape objects:

var yaml = /*lang=yaml*/
    """
    - type: rectangle
      # rest of the YAML from above
    """;

var shapes = deserializer.Deserialize<Shape[]>(yaml);

Inspecting the shapes variable using Rider’s collection visualizer confirms the correct types have been deserialized. To help with debugging, I used the [DebuggerDisplay] attribute to display a user-friendly string representation of the shapes:

Rider collection visualizer showing the deserialized shapes

You can learn more about YamlDotNet’s polymorphic deserialization in the official documentation. I skipped an example of another kind of discriminator based on the existence of a specific property in the C# class, as it’s a less common use case.

Licensed under CC BY 4.0
Ko-fi donations Buy me a coffee