Recursive AI

Getting started with oneyaml for C#

All oneyaml C# docs

Install OneYaml for .NET, read YAML into C# types, write it back, edit a file in place without losing its comments, and apply JSON Patch and Merge Patch.

OneYaml is lossless, comment-preserving editing and a System.Text.Json-style serializer for YAML 1.2, in one package. Both run on one parser, so a typed read and an edit never disagree about what a file means. Every sample on this page was compiled and run against OneYaml 1.2.1 from NuGet on .NET 10; the output under each one is what it printed.

Install

OneYaml supports .NET 8 and .NET 10.

Terminal
dotnet new console -o YamlStart
cd YamlStart
dotnet add package OneYaml --version 1.2.1

The namespace is OneYaml. The package has no dependencies outside .NET and no unsafe code, and a source generator in the package covers trimmed and native AOT apps. The license is MIT or Apache-2.0, at your choice.

The samples are complete Program.cs files with top-level statements. A few read a file from the current directory; the YAML they read is shown with them.

Read YAML into a type

YamlSerializer.Deserialize<T> binds YAML to classes and records the way JsonSerializer binds JSON. Keys bind in camel case by default, so replicas fills Replicas.

C#
using OneYaml;
 
var cfg = YamlSerializer.Deserialize<Config>("""
    name: api
    replicas: 2
    ports: [80, 443]
    debug: false
    """)!;
string ports = string.Join(", ", cfg.Ports);
Console.WriteLine($"{cfg.Name} runs {cfg.Replicas} replicas on {ports} (debug: {cfg.Debug})");
 
record Config(string Name, int Replicas, List<int> Ports, bool Debug);
Text
api runs 2 replicas on 80, 443 (debug: False)

YAML 1.2 is the default: yes is a string and 0755 is the integer 755. A value that doesn’t fit its type is a YamlException with a line, a column and a path (see errors with line and column).

Write YAML

YamlSerializer.Serialize writes any value as block-style YAML.

C#
using OneYaml;
 
var cfg = new Config("api", 3, [80, 443], new() { ["tier"] = "web" });
Console.Write(YamlSerializer.Serialize(cfg));
 
record Config(
    string Name,
    int Replicas,
    List<int> Ports,
    Dictionary<string, string> Labels);
Text
name: api
replicas: 3
ports:
  - 80
  - 443
labels:
  tier: web

YamlSerializerOptions sets naming policies, ignore conditions, converters and more, as JsonSerializerOptions does (Serializer).

Edit a file and keep its comments

To change a file that people also edit by hand, parse it into a YamlDocument instead. An edit changes only what you touch, in the old value’s style; comments and layout elsewhere stay byte for byte.

C#
using OneYaml;
 
using var doc = YamlDocument.Parse("""
    # Deploy settings
    name: api
    replicas: 2   # raised by the release job
    ports: [80]
 
    """);
 
// Read a value by JSON Pointer.
Console.WriteLine($"replicas = {doc.Get<int>("/replicas")}");
 
// Change a value, append to a list, add a key.
doc.Set("/replicas", 3);
doc.Add("/ports/-", 443);
doc.Root["region"] = "us-east-1";
 
Console.Write(doc);
Text
replicas = 2
# Deploy settings
name: api
replicas: 3   # raised by the release job
ports: [80, 443]
region: us-east-1

Paths are JSON Pointers (RFC 6901): /replicas, /spec/containers/0/image, and /ports/- for the end of a list. doc.Root reads like a JsonNode: node["key"], node[0], and assignment to change a value. A new value copies the style of its neighbors, so the flow list [80] stays a flow list (Editing).

Apply a JSON Patch or a Merge Patch

A YamlDocument applies RFC 6902 JSON Patch and RFC 7396 Merge Patch to the YAML text, atomically: if any operation fails, the document is unchanged.

C#
using OneYaml;
 
const string Src = """
    # Web service
    image: web:1.0   # pinned
    env:
      DEBUG: "1"
      LOG: info
    ports: [80]
 
    """;
 
// JSON Patch (RFC 6902): all or nothing.
using (var doc = YamlDocument.Parse(Src))
{
    doc.ApplyPatch("""
        [
          { "op": "replace", "path": "/image", "value": "web:1.1" },
          { "op": "add", "path": "/ports/-", "value": 443 }
        ]
        """);
    Console.Write(doc);
}
Console.WriteLine("---");
 
// Merge Patch (RFC 7396): null removes a key.
using (var doc = YamlDocument.Parse(Src))
{
    doc.ApplyMergePatch("""{ "env": { "DEBUG": null, "LOG": "warn" } }""");
    Console.Write(doc);
}
Text
# Web service
image: web:1.1   # pinned
env:
  DEBUG: "1"
  LOG: info
ports: [80, 443]
---
# Web service
image: web:1.0   # pinned
env:
  LOG: warn
ports: [80]

Patches use the YAML data model: test compares YAML values, so 1 and 1.0 differ. ApplyPatch also takes a JsonNode or a YamlPatch built in code (Patches).

Where next

Recipes, each a complete program:

The docs:

Choosing a C# YAML library: YamlDotNet, SharpYaml, VYaml and OneYaml compared.