YAML (YAML Ain’t Markup Language) is a human-readable data serialization language, while YamlDotNet is a .NET library that helps us serialize and deserialize YAML data. Due to its simple and intuitive syntax, programming languages frequently use YAML for configuration files and data exchange.

Its flexibility in handling complex data structures has made YAML popular in modern software development, particularly in web development and DevOps. Many popular tools and frameworks, such as Kubernetes, Ansible, and Docker rely heavily on YAML for configuration management.

In this article, we’ll explore serialization and deserialization techniques using the YamlDotNet library.

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!
To download the source code for this article, you can visit our GitHub repository.

Let’s dive in.

What is YamlDotNet?

The YamlDotNet open-source library helps us serialize and deserialize data in YAML format. It provides a simple and intuitive API for converting .NET objects to YAML strings and vice versa.

First, let’s add the YamlDotNet NuGet package to our project. The latest version available as of this writing is 15.1.2:

dotnet add package YamlDotNet

Now that the package is installed, let’s explore how to serialize and deserialize YAML, starting with simple objects and moving to more complex scenarios.

Basic Serialization/Deserialization With YamlDotNet

First, let’s create a simple Product class:

public class Product
{
    public int Id { get; set; }
    public required string Name { get; set; }
    public decimal Price { get; set; }
}

Next, let’s create a generic SerializeAndDeserialize class, responsible for serializing and deserializing data:

public static class SerializeAndDeserialize
{
    public static string Serialize<T>(T obj)
    {
        var serializer = new SerializerBuilder().Build();

        return serializer.Serialize(obj);
    }

    public static T Deserialize<T>(string yaml)
    {
        var deserializer = new DeserializerBuilder().Build();

        return deserializer.Deserialize<T>(yaml);
    }
}

Here, we have two methods. The Serialize() method converts an object of any type into a YAML string. We achieve this by creating an instance of SerializerBuilder and calling the Build() method to get a Serializer object. Then we call the Serialize() method on this object, passing in the object we want to serialize. The method returns the serialized YAML string.

Next, the Deserialize() method takes a YAML string and converts it back into an object of a specified type. We create an instance of DeserializerBuilder and call the Build() method to get a Deserializer object. We then call the Deserialize() method on this object, passing in the YAML string. The method returns the deserialized object.

Let’s use this class to serialize a Product object into YAML:

var yamlProduct = SerializeAndDeserialize.Serialize(new Product
{
    Id = 1,
    Name = "Apple",
    Price = 1.99m
});
Console.WriteLine(yamlProduct);

Our code outputs the YAML representation of the Product object:

Id: 1
Name: Apple
Price: 1.99

Now let’s see deserialize it:

var deserializeProduct = SerializeAndDeserialize.Deserialize(yamlProduct);
Console.WriteLine($"Name: {deserializeProduct.Name}, Price: {deserializeProduct.Price}");

We deserialize the YAML string back into a Product object using the Deserialize() method from the same class, then print the Name and Price to the console:

Name: Apple, Price: 1.99

Let’s see if we can perform the same transformation on a more complex object.

Complex Object Graph Serialization/Deserialization

Now, let’s create an object graph consisting of three related classes:

public class Manufacturer
{
    public required string Name { get; set; }
    public required string Country { get; set; }
}

public class Item
{
    public required string Name { get; set; }
    public decimal Price { get; set; }
    public required Manufacturer Manufacturer { get; set; }
}

public class Store
{
    public required string Name { get; set; }
    public required List<Item> Items { get; set; }
}

Here we have a more complex object graph. We have a Store class that contains a collection of Item objects, each of which has a Manufacturer.

The code for serializing and deserializing the Store object is the same as for the Product object, so let’s create a new store and then serialize it:

var yamlStore = SerializeAndDeserialize.Serialize(new Store
{
    Name = "Tech Store",
    Items =
            [
                new Item
                {
                    Name = "Laptop",
                    Price = 1000m,
                    Manufacturer = new Manufacturer
                    {
                        Name = "Tech Corp",
                        Country = "USA"
                    }
                },
                new Item
                {
                    Name = "Smartphone",
                    Price = 500m,
                    Manufacturer = new Manufacturer
                    {
                        Name = "Mobile Inc",
                        Country = "China"
                    }
                }
            ]
});

Console.WriteLine(yamlStore);

Here, we’ve defined a list of Item objects using collection expressions and used that collection to create a new Store. We then serialized the Store via our Serialize() method. Finally, we printed the resulting YAML string to the console:

Name: Tech Store
Items:
- Name: Laptop
  Price: 1000
  Manufacturer:
    Name: Tech Corp
    Country: USA
- Name: Smartphone
  Price: 500
  Manufacturer:
    Name: Mobile Inc
    Country: China

Serializing a Store object to a YAML string worked the same as it did for the more basic Product object. Even with a more complex object, our serialization code remained the same, one of the advantages of using the YamlDotNet library.

The deserialization process works the same as before too, so rather than exploring that, let’s take a look at another feature of YamlDotNet – validation during deserialization.

Validation at the Time of Deserialization

To ensure that a deserialized object has the correct structure and properties, it is important to validate it. Fortunately, YamlDotNet makes this possible with the use of the INodeDeserializer interface. By implementing this interface, we can actively validate an object to ensure that it meets our requirements and throws any necessary exceptions when the data is invalid:

public class DeserializerValidation(INodeDeserializer nodeDeserializer) : INodeDeserializer
{
    public bool Deserialize(IParser reader, Type expectedType, Func<IParser, Type, object?> nestedObjectDeserializer,
        out object? value)
    {
        if (!nodeDeserializer.Deserialize(reader, expectedType, nestedObjectDeserializer, out value))
            return false;

        var context = new ValidationContext(value);
        var results = new List<ValidationResult>();
        if (Validator.TryValidateObject(value, context, results, true))
            return true;

        var message = string.Join(NewLine, results.Select(r => r.ErrorMessage));
        throw new YamlException(message);
    }
}

Our DeserializerValidation class implements the INodeDeserializer interface and accepts an INodeDeserializer object in the constructor. This parameter is used in the Deserialize() method, which calls nodeDeserializer.Deserialize() and attempts to deserialize the YAML data into an object of the expected type. If the deserialization fails, we return false and end the process.

However, if the deserialization process succeeds, we validate the deserialized object. We create a ValidationContext for the object and a list to hold the validation results. Then, we use the Validator.TryValidateObject() method to validate the object and return true if the object is valid.

On the other hand, if the object is not valid, we compile a list of validation error messages and throw a YamlException with these messages to inform the caller that the validation has failed.

Using the DeserializerValidation Class

Now let’s see validation in action. First, let’s define a Person class:

public class Person
{
    [Required]
    public string Name { get; set; }

    public int Age { get; set; }
}

To indicate that the Name is required, we use the Required attribute from the System.ComponentModel.DataAnnotations namespace. Now, let’s test out our DeserializerValidation class with an invalid YAML string:

var personYaml = @"Name: ~";

var deserializer
    = new DeserializerBuilder()
      .WithNodeDeserializer(i => new DeserializerValidation(i),
                            s => s.InsteadOf<ObjectNodeDeserializer>()).Build();

try
{
    deserializer.Deserialize<Person>(personYaml);
}
catch (YamlException e)
{
    Console.WriteLine($"Unable to deserialize person: {e.Message}");
}

We start by creating a YAML string representing a Person object but with an invalid value. Specifically, the Name field is set to null, which in YAML is indicated by the tilde (~) symbol. 

To deserialize the YAML string into a Person object, we use the DeserializerBuilder class from the YamlDotNet library to create a new deserializer. However, we don’t use the default ObjectNodeDeserializer, but rather our DeserializerValidation class to validate the deserialized object against its data annotations. We accomplish this with the WithNodeDeserializer() extension method, inserting our custom DeserializerValidation object. We then indicate, via InsteadOf(), that we wish to use our custom validator in place of the default. 

Finally, we attempt to deserialize the invalid YAML string into a Person object within a try block. However, since the Name field is required and we have provided a null value, the deserialization fails and throws a YamlException. To handle this exception, we catch it in the catch block and display a message in the console:

Unable to deserialize person: The Name field is required.

JSON Support in YamlDotNet

Let’s take a look at another useful feature, JSON serialization support.

We might find ourselves needing to convert YAML to JSON:

public static class JsonSupport
{
    public static string SerializeToJson(string yaml)
    {
        var deserializer = new DeserializerBuilder().Build();
        var yamlObject = deserializer.Deserialize(yaml);
        var serializer = new SerializerBuilder().JsonCompatible().Build();
        return serializer.Serialize(yamlObject);
    }
}

We start by defining a static method named SerializeToJson(). This method takes a YAML string as an input. Next, we create a deserializer using the DeserializerBuilder class, which converts the input YAML string into an object. Following this, we create a serializer using the SerializerBuilder class. We make this serializer JSON compatible by calling the JsonCompatible() method. This serializer converts the deserialized YAML object into a JSON string. Finally, we return the JSON string.

Let’s use this class to serialize an object to JSON:

var yamlPerson = """
                Name: John Doe
                Age: 25
                """;
Console.WriteLine($"Json String: {JsonSupport.SerializeToJson(yamlPerson)}");

We create a string representing the Person object in YAML format, then pass it to the SerializeToJson() method and receive the JSON object:

{"Name": "John Doe", "Age": "25"}

The YAML representation of the Person object was successfully transformed into JSON format.

Conclusion

YAML is a language that humans can easily read and write. Many different tools and frameworks use it for configuration management. Developers can use the YamlDotNet library to serialize and deserialize simple and complex .NET objects. It validates objects during deserialization and can convert YAML to JSON format.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!