---
title: "Write a custom YAML converter in C#"
url: https://recursiveai.org/oneyaml/csharp/cookbook/custom-yaml-converter/
description: "Write a custom YAML converter in C# with a complete runnable example, real output and the behavior to check before using the result. Free, open-source .NET code."
version: "1.2.1"
---

# Write a custom YAML converter in C#

**OneYaml** is a free, open-source YAML library for .NET from Recursive AI. MIT or Apache-2.0, no native dependencies.

[NuGet](https://www.nuget.org/packages/OneYaml) · [GitHub](https://github.com/recursive-ai-oss/oneyaml-csharp) · [Docs](https://recursiveai.org/oneyaml/csharp/docs/) · [Download example (.zip)](https://recursiveai.org/oneyaml/csharp/cookbook/custom-yaml-converter/custom-yaml-converter.zip)

Store a .NET `Version` as one YAML scalar, such as `2.1.0`.

```shell
dotnet add package OneYaml
```

The package targets .NET 8 and .NET 10; the download runs on [.NET 10](https://dotnet.microsoft.com/download/dotnet/10.0).

```csharp
using OneYaml;

var options = new YamlSerializerOptions
{ Converters = { new VersionConverter() } };
Console.Write(YamlSerializer.Serialize(
    new Version(2, 1, 0), options));

sealed class VersionConverter
    : YamlConverter<Version>
{
    public override Version Read(
        ref Utf8YamlReader reader, Type type,
        YamlSerializerOptions options) =>
        Version.TryParse(reader.GetString(),
            out var value) ? value
            : throw reader.CreateException(
                "Bad version.");
    public override void Write(
        Utf8YamlEmitter writer, Version value,
        YamlSerializerOptions options) =>
        writer.WriteStringValue(value.ToString());
}
```

```text
2.1.0
```

Unzip [the example](https://recursiveai.org/oneyaml/csharp/cookbook/custom-yaml-converter/custom-yaml-converter.zip), then `dotnet run`.

## The full program

The full program round-trips this inline input into a typed `Release` record:

```yaml
name: app
version: 2.1.0
```

It also tries `version: two`. The converter reports invalid versions with `reader.CreateException`, which supplies the input line, column and path.

```csharp
using OneYaml;

var options = new YamlSerializerOptions
{
    Converters = { new VersionConverter() }
};
var release = YamlSerializer.Deserialize<Release>(
    "name: app\nversion: 2.1.0\n", options)!;
string yaml = YamlSerializer.Serialize(release, options);
Console.Write(yaml);
var copy = YamlSerializer.Deserialize<Release>(yaml, options)!;
Console.WriteLine($"Round-trip: {copy == release}");
try
{
    YamlSerializer.Deserialize<Release>(
        "name: app\nversion: two\n", options);
}
catch (YamlException error)
{
    Console.WriteLine(error.Message);
}

record Release(string Name, Version Version);

sealed class VersionConverter : YamlConverter<Version>
{
    public override Version Read(ref Utf8YamlReader reader,
        Type type, YamlSerializerOptions options)
    {
        if (reader.TokenType != YamlTokenKind.Scalar ||
            !Version.TryParse(reader.GetString(), out var version))
            throw reader.CreateException("Expected a version scalar.");
        return version;
    }

    public override void Write(Utf8YamlEmitter writer,
        Version value, YamlSerializerOptions options) =>
        writer.WriteStringValue(value.ToString());
}
```

`dotnet run` prints:

```text
name: app
version: 2.1.0
Round-trip: True
Expected a version scalar. (line 2, column 10, path /version)
```

## Variations

To read just a scalar, keep the same `VersionConverter` class and replace the top-level statements with:

```csharp
using OneYaml;

var options = new YamlSerializerOptions
{ Converters = { new VersionConverter() } };
var version = YamlSerializer.Deserialize<Version>("3.4.5", options)!;
Console.WriteLine(version.Minor);
```

```text
4
```

## Related recipes

- [Parse YAML into classes](https://recursiveai.org/oneyaml/csharp/cookbook/parse-yaml-into-classes/)
- [Write YAML](https://recursiveai.org/oneyaml/csharp/cookbook/write-yaml/)

## The short answer

Derive from `YamlConverter<T>`, implement `Read` and `Write`, and add an instance to `YamlSerializerOptions.Converters`. Pass those options on both serialization and deserialization. In this example the wire value is one version string, while your C# code receives a `System.Version`. The typed record does not need to know how that scalar is represented.

Use a converter when a type has a compact representation that differs from its public properties, or when its input needs domain-specific checks. Use ordinary serialization when the YAML mapping already matches the class. A converter is not required just to rename properties or change indentation.

## How do I customize YAML serialization in C#?

The `Write` method must emit exactly one value. `WriteStringValue(value.ToString())` is enough for `Version`: it lets the emitter choose the quoting rather than building YAML text by hand. The full program registers one options-level converter and uses it for the `Version` property inside `Release`, not just for a top-level scalar.

`Read` receives the reader already positioned on the first token of that value. Leave it on the last token of the value. For a scalar those are the same token, so do not call `Read()` after reading the string. The serializer advances to the next property. For a mapping or sequence, the converter must instead finish on its matching end token. See the [reader contract](https://recursiveai.org/oneyaml/csharp/docs/reader-emitter/) for token kinds and `Skip()`.

The complete program checks `TokenType` before interpreting a scalar, then uses `Version.TryParse`. If either check fails it throws a binding error at the current position. This gives callers a useful location instead of an unrelated parsing exception. It also avoids treating an entire mapping as if it were a version string.

## How do I deserialize YAML into a custom C# type?

Pass the options containing the converter to `Deserialize<T>`. The converter applies when the serializer encounters the target type, including inside a record, list or other object. The input shown above has a `name` string and a `version` scalar; `Release.Version` is still strongly typed after reading it.

The round-trip line compares the resulting record with the original. It demonstrates preservation of the typed values, not preservation of YAML whitespace or comments. Serialization creates YAML from the object model; use the document editing API if retaining a file’s presentation is the goal.

Converter selection has a defined precedence: an attribute on the member wins over the options list, which wins over an attribute on the type. That order applies to reflection and generated metadata. These rules and `HandleNull`, which controls whether `Read` is called for null, are documented in the [serializer guide](https://recursiveai.org/oneyaml/csharp/docs/serializer/). Registering an options converter is therefore not an unconditional override of every member’s own converter.
