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.
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.