Recursive AI

Parse YAML into C# classes

All oneyaml C# recipes

Read a YAML file into typed C# data: a class with defaults, a record, a dictionary, an optional section and an enum. YamlSerializer takes options shaped like JsonSerializerOptions, and honors the System.Text.Json attributes where no YAML attribute is present.

The file:

YAML
# service.yaml
name: web
image: nginx:1.27
env:
  LOG_LEVEL: info
  PORT: "8080"
healthcheck:
  path: /healthz
  interval-seconds: 10
restart: on-failure

The program:

C#
using System.Text.Json;
using OneYaml;
 
var options = new YamlSerializerOptions
{
    PropertyNamingPolicy = JsonNamingPolicy.KebabCaseLower,
    EnumNamingPolicy = JsonNamingPolicy.KebabCaseLower,
    UnmappedMemberHandling = YamlUnmappedMemberHandling.Disallow,
    SourceName = "service.yaml",
};
 
var svc = YamlSerializer.Deserialize<Service>(File.ReadAllText("service.yaml"), options)!;
Console.WriteLine($"{svc.Name} runs {svc.Image} x{svc.Replicas}, restart {svc.Restart}");
Console.WriteLine($"env: {string.Join(", ", svc.Env.Select(e => $"{e.Key}={e.Value}"))}");
if (svc.Healthcheck is { } h)
{
    Console.WriteLine($"health check: {h.Path} every {h.IntervalSeconds}s");
}
 
// Disallow: a key the type doesn't have is an error, not ignored.
try
{
    YamlSerializer.Deserialize<Service>("name: web\nimgae: nginx\n", options);
}
catch (YamlException e)
{
    Console.WriteLine(e.Message);
}
 
class Service
{
    [YamlRequired]
    public string Name { get; set; } = "";
    public string Image { get; set; } = "";
    public int Replicas { get; set; } = 1;
    public Dictionary<string, string> Env { get; set; } = [];
    public Healthcheck? Healthcheck { get; set; }
    public Restart Restart { get; set; }
}
 
record Healthcheck(string Path, int IntervalSeconds);
 
enum Restart { Always, OnFailure, Never }
Text
web runs nginx:1.27 x1, restart OnFailure
env: LOG_LEVEL=info, PORT=8080
health check: /healthz every 10s
service.yaml: The key 'imgae' is not a member of Service. (line 2, column 1, path /imgae)

Good to know

  • Naming: keys bind in camel case by default. Here PropertyNamingPolicy and EnumNamingPolicy are kebab case, so interval-seconds fills IntervalSeconds and on-failure reads as Restart.OnFailure.
  • Defaults are the property initializers: replicas isn’t in the file, so Replicas stays 1. [YamlRequired] makes a missing key an error.
  • Unknown keys: UnmappedMemberHandling = YamlUnmappedMemberHandling.Disallow makes a key the type doesn’t have an error with its position and path. SourceName puts the file name in front of every message.
  • PORT: "8080" is quoted, so it is a string. YAML 1.2 is the default: plain yes is a string, and 0755 is the integer 755.
  • [YamlPropertyName], [YamlConverter] and polymorphism work as their System.Text.Json counterparts do (Serializer).

When the shape isn’t known ahead of time, read nodes from a YamlDocument: read YAML without a class.