Updated on

YamlDotNet is the library .NET uses to read and write YAML, because the framework ships no YAML support of its own. Two builders do almost all the work: SerializerBuilder turns an object into a YAML string, and DeserializerBuilder turns a YAML string back into an object.

The YAML 1.2.2 specification calls it “a human-friendly, cross language, Unicode based data serialization language” (YAML 1.2.2 spec), and that matters more than it used to: Kubernetes manifests, Docker Compose files, GitHub Actions workflows, and Ansible playbooks are all YAML, so parsing it is now a normal part of writing .NET tooling rather than a niche requirement.

To download the source code for this article, you can visit our GitHub repository.

What Is YamlDotNet?

YamlDotNet is an open-source .NET library for serializing objects to YAML and deserializing YAML back into objects. .NET has no built-in YAML support, so for anything beyond hand-parsing strings this is the library the ecosystem settled on.

The API is two builders. new SerializerBuilder().Build() produces a serializer whose Serialize(obj) returns a YAML string, and new DeserializerBuilder().Build() produces a deserializer whose Deserialize<T>(yaml) returns a typed object.

Both builders are where configuration happens. Naming conventions, indentation, how nulls and defaults are emitted, and custom type converters are all chained onto the builder before Build(), and the built instance is safe to reuse.

It handles nested objects, collections, and dictionaries without extra work, and because YAML is a superset of JSON, the same deserializer reads JSON documents too.

The one thing to watch is that deserializing into a type with required or non-nullable members will not fail on missing keys by itself: validation is something we add.

Add the NuGet package to the project. Unlike XML serialization in C#, which the framework supports out of the box, YAML needs this dependency:

dotnet add package YamlDotNet

How Do We Serialize and Deserialize YAML in C#?

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 same serialization and deserialization in C# pattern applied to YAML. 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 deserialize it:

var deserializeProduct = SerializeAndDeserialize.Deserialize<Product>(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

Is this material useful to you? Consider subscribing and get ASP.NET Core Web API Best Practices eBook for FREE!

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 look at what happens when the YAML keys don’t match our C# property names.

How Do We Map YAML Names to C# Property Names?

By default, YamlDotNet uses our C# property names verbatim, so a FirstName property becomes a FirstName key. Real-world YAML rarely looks like that, and the mismatch is the most common reason deserialization silently returns nulls.

Kubernetes and Docker Compose use hyphens, Ansible uses underscores, and JSON-derived formats use camelCase. We fix that once on the builder rather than per property:

var deserializer = new DeserializerBuilder()
    .WithNamingConvention(HyphenatedNamingConvention.Instance)
    .Build();

The same convention should be set on the serializer, otherwise we read one shape and write another.

Where a single key does not follow the convention, [YamlMember(Alias = "api-version")] overrides it for that property alone.

The failure mode is worth knowing because it is quiet: an unmatched key is ignored, the property keeps its default, and nothing throws. If deserialization returns an object with everything null, the naming convention is the first thing to check.

YamlDotNet ships a naming convention for each common style:

Convention classYAML key produced from FirstNameTypical source
NullNamingConvention (default)FirstName.NET-to-.NET round trips
CamelCaseNamingConventionfirstNameJSON-adjacent APIs
PascalCaseNamingConventionFirstNameexplicit .NET style
UnderscoredNamingConventionfirst_nameAnsible, many CLI tools
HyphenatedNamingConventionfirst-nameKubernetes, Docker Compose, GitHub Actions
LowerCaseNamingConventionfirstnameflat legacy config

Configuration files are where this bites most often, the same care over key names that pays off when reading appsettings.json in a .NET console application.

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 throw 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, ObjectDeserializer rootDeserializer)
    {
        if (!nodeDeserializer.Deserialize(reader, expectedType, nestedObjectDeserializer, out value, rootDeserializer))
            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.

Is this material useful to you? Consider subscribing and get ASP.NET Core Web API Best Practices eBook for FREE!

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

That is a design goal, not a coincidence. Of the 1.2 revision, the YAML specification says: “Its primary focus was making YAML a strict superset of JSON.”

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. If we are already working with System.Text.Json, this lets us reuse that pipeline for YAML input too.

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, and many different tools and frameworks use it for configuration management. With YamlDotNet, we serialize and deserialize both simple and complex .NET objects, map YAML keys to C# properties with a naming convention, validate objects during deserialization, and convert YAML to JSON, all through the same two builders.

Tested with .NET 10.0.10 and YamlDotNet 18.1.0.