# oneyaml C# docs, all pages (OneYaml 1.2.1) Every page of https://recursiveai.org/oneyaml/csharp/docs/ and https://recursiveai.org/oneyaml/csharp/cookbook/ as markdown, getting started first. Index: https://recursiveai.org/oneyaml/csharp/llms.txt # Getting started with oneyaml for C# 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. ```sh 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` binds YAML to classes and records the way `JsonSerializer` binds JSON. Keys bind in camel case by default, so `replicas` fills `Replicas`. ```csharp using OneYaml; var cfg = YamlSerializer.Deserialize(""" 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 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](https://recursiveai.org/oneyaml/csharp/cookbook/errors-with-line-and-column/)). ## Write YAML `YamlSerializer.Serialize` writes any value as block-style YAML. ```csharp 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 Ports, Dictionary 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](https://recursiveai.org/oneyaml/csharp/docs/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. ```csharp 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("/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](https://recursiveai.org/oneyaml/csharp/docs/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. ```csharp 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](https://recursiveai.org/oneyaml/csharp/docs/patches/)). ## Where next Recipes, each a complete program: - [Parse YAML into C# classes](https://recursiveai.org/oneyaml/csharp/cookbook/parse-yaml-into-classes/) and [read YAML without a class](https://recursiveai.org/oneyaml/csharp/cookbook/read-yaml-without-a-class/) - [Edit YAML and keep its comments](https://recursiveai.org/oneyaml/csharp/cookbook/edit-yaml-keep-comments/) and [merge or patch YAML](https://recursiveai.org/oneyaml/csharp/cookbook/patch-yaml/) - [Convert YAML to JSON and back](https://recursiveai.org/oneyaml/csharp/cookbook/yaml-to-json/) and [read Kubernetes manifests and multi-document files](https://recursiveai.org/oneyaml/csharp/cookbook/kubernetes-multi-document/) - [Report errors with line and column](https://recursiveai.org/oneyaml/csharp/cookbook/errors-with-line-and-column/), [anchors, aliases and merge keys](https://recursiveai.org/oneyaml/csharp/cookbook/anchors-aliases-merge-keys/) - [Read untrusted YAML safely](https://recursiveai.org/oneyaml/csharp/cookbook/untrusted-input/), [use YAML in a native AOT app](https://recursiveai.org/oneyaml/csharp/cookbook/native-aot/) and [switch from YamlDotNet with one package](https://recursiveai.org/oneyaml/csharp/cookbook/switch-from-yamldotnet/) The docs: - [Serializer](https://recursiveai.org/oneyaml/csharp/docs/serializer/), [editing](https://recursiveai.org/oneyaml/csharp/docs/editing/), [patches](https://recursiveai.org/oneyaml/csharp/docs/patches/), [limits](https://recursiveai.org/oneyaml/csharp/docs/limits/) and the [reader and emitter](https://recursiveai.org/oneyaml/csharp/docs/reader-emitter/). The API reference is the XML documentation your IDE shows. - Moving from another library: [YamlDotNet](https://recursiveai.org/oneyaml/csharp/docs/migrate-yamldotnet/) and [SharpYaml](https://recursiveai.org/oneyaml/csharp/docs/migrate-sharpyaml/), with the compatibility packages [OneYaml.Compat.YamlDotNet](https://recursiveai.org/oneyaml/csharp/docs/compat-yamldotnet/) and [OneYaml.Compat.SharpYaml](https://recursiveai.org/oneyaml/csharp/docs/compat-sharpyaml/). - Choosing a library: [how it compares](https://recursiveai.org/oneyaml/csharp/docs/comparison/), [performance](https://recursiveai.org/oneyaml/csharp/docs/performance/) and [conformance](https://recursiveai.org/oneyaml/csharp/docs/conformance/). - What changed and what stays stable: the [changelog](https://recursiveai.org/oneyaml/csharp/docs/changelog/) and [stability](https://recursiveai.org/oneyaml/csharp/docs/stability/). Choosing a C# YAML library: [YamlDotNet, SharpYaml, VYaml and OneYaml compared](https://recursiveai.org/oneyaml/articles/csharp-yaml-parser/). # oneyaml-csharp **Lossless, comment-preserving editing and a System.Text.Json-style serializer for YAML 1.2, in one .NET package.** Both run on one parser, so a typed read and an edit never disagree about what a file means. Pure C#, correct YAML 1.2 by default; nesting, alias expansion and integer size are bounded unless you lift the limits. [![NuGet](https://img.shields.io/nuget/v/OneYaml)](https://www.nuget.org/packages/OneYaml) [![CI](https://github.com/recursive-ai-oss/oneyaml-csharp/actions/workflows/ci.yml/badge.svg)](https://github.com/recursive-ai-oss/oneyaml-csharp/actions/workflows/ci.yml) [![.NET 8 and 10](https://img.shields.io/badge/.NET-8.0%20%7C%2010.0-512BD4)](https://recursiveai.org/oneyaml/csharp/docs/stability/) [![License](https://img.shields.io/badge/license-MIT%20OR%20Apache--2.0-blue)](#license) How many times faster oneyaml-csharp is than the strongest other .NET YAML library on each task, on real configuration files (wall-clock time, log scale, higher is better; a task where oneyaml-csharp is slower shows as a slower bar): Bar chart: how many times faster oneyaml-csharp is than the strongest competitor on each task, log scale, higher is better. 7 tasks: 5 better, 0 level, 1 mixed, 0 worse, 1 not comparable. Speeds are over real-world workloads. A mark counts every workload. Loses only outside the real-world workloads: unchanged emit. [Full results, with memory and the method →](https://github.com/recursive-ai-oss/oneyaml-csharp/blob/v1.2.1/docs/performance.md) ## Why oneyaml-csharp - **Edits that keep your file.** `YamlDocument` changes only what you touch, in the old value's style; comments and layout elsewhere stay byte for byte. JSON Patch and Merge Patch apply the same way ([editing](https://recursiveai.org/oneyaml/csharp/docs/editing/), [patches](https://recursiveai.org/oneyaml/csharp/docs/patches/)). - **A serializer you already know.** Options, naming policies, converters, polymorphism and the System.Text.Json attributes, with a source generator in the package for trimmed and native AOT apps ([serializer](https://recursiveai.org/oneyaml/csharp/docs/serializer/)). - **Correct YAML 1.2 by default.** `yes` is a string and `0755` the integer 755; YAML 1.1 forms come only from `MergeKeys` or the compatibility packages. Checked against the full yaml-test-suite, with every deviation listed ([conformance](https://recursiveai.org/oneyaml/csharp/docs/conformance/)). - **Safe on outside input.** Depth, alias expansion and integer size are bounded by default; input size, scalar size and document count are bounded when you set them, and `YamlLimits.Untrusted` sets them ([limits](https://recursiveai.org/oneyaml/csharp/docs/limits/)). No unsafe code, no dependencies outside .NET, and fuzzed ([fuzzing](https://github.com/recursive-ai-oss/oneyaml-csharp/blob/7818048c4be434333767c47d66176fc626d1da8e/tests/OneYaml.Fuzz/README.md)). - **Fast, with the losses shown.** Ahead of every other .NET YAML library at typed load, generic load, dump, document parse, editing and writing back an unedited file, through reflection and generated metadata alike, and level or lower on memory throughout. Not on every file: writing back some unedited files was slower than SharpYaml's syntax tree in our run, the generated-metadata column was slower on a few tiny files, and a few files are level ([every loss](https://recursiveai.org/oneyaml/csharp/docs/performance/#results-at-a-glance)). [How oneyaml-csharp compares](https://recursiveai.org/oneyaml/csharp/docs/comparison/) with YamlDotNet, SharpYaml and VYaml, including when one of them is the better choice. ## Install ```sh dotnet add package OneYaml ``` Moving from YamlDotNet or SharpYaml? A [compatibility package](#packages) covers the common serializer and model calls; its guide lists what it leaves out and what differs. ## Quick start Change two values in a file, keeping the comment, the block scalar and every other byte as they were; then bind the original text to a record. ```csharp using OneYaml; string src = """ # Build settings script: | make build replicas: 2 # the CI job scales this """; using var doc = YamlDocument.Parse(src); doc.Set("/script", "make build\nmake test\n"); // still a block literal doc.Root["replicas"] = 3; // only the 2 changes Console.WriteLine(doc); // # Build settings // script: | // make build // make test // replicas: 3 # the CI job scales this var cfg = YamlSerializer.Deserialize(src)!; Console.WriteLine($"{cfg.Replicas} replicas"); // 2 replicas record Config(int Replicas, string Script); ``` Keys bind in camel case by default. Read [what an edit keeps, and what it does not](https://recursiveai.org/oneyaml/csharp/docs/editing/#what-an-edit-keeps-and-what-it-does-not); for native AOT, use [generated metadata](https://recursiveai.org/oneyaml/csharp/docs/serializer/#source-generation-and-native-aot). ## Where to go - **Using it:** [serializer](https://recursiveai.org/oneyaml/csharp/docs/serializer/), [editing](https://recursiveai.org/oneyaml/csharp/docs/editing/), [patches](https://recursiveai.org/oneyaml/csharp/docs/patches/), [limits](https://recursiveai.org/oneyaml/csharp/docs/limits/), [reader and emitter](https://recursiveai.org/oneyaml/csharp/docs/reader-emitter/). The API reference is the XML documentation your IDE shows. - **Migrating:** [from YamlDotNet](https://recursiveai.org/oneyaml/csharp/docs/migrate-yamldotnet/), [from SharpYaml](https://recursiveai.org/oneyaml/csharp/docs/migrate-sharpyaml/). - **Choosing a library:** [how oneyaml-csharp compares](https://recursiveai.org/oneyaml/csharp/docs/comparison/), including where the others win. - **Evidence:** [benchmark tables](https://recursiveai.org/oneyaml/csharp/docs/performance/#results-at-a-glance) (the numbers behind the chart, every loss and the method), [yaml-test-suite conformance](https://recursiveai.org/oneyaml/csharp/docs/conformance/), [fuzzing](https://github.com/recursive-ai-oss/oneyaml-csharp/blob/7818048c4be434333767c47d66176fc626d1da8e/tests/OneYaml.Fuzz/README.md). - **What changed, and what stays stable:** [CHANGELOG.md](https://recursiveai.org/oneyaml/csharp/docs/changelog/), [stability and supported .NET versions](https://recursiveai.org/oneyaml/csharp/docs/stability/). - **Contributing and security:** [CONTRIBUTING.md](https://github.com/recursive-ai-oss/oneyaml-csharp/blob/7818048c4be434333767c47d66176fc626d1da8e/CONTRIBUTING.md), [SECURITY.md](https://github.com/recursive-ai-oss/oneyaml-csharp/blob/7818048c4be434333767c47d66176fc626d1da8e/SECURITY.md), [CODE_OF_CONDUCT.md](https://github.com/recursive-ai-oss/oneyaml-csharp/blob/7818048c4be434333767c47d66176fc626d1da8e/CODE_OF_CONDUCT.md). - **Other languages:** [oneyaml-go](https://github.com/recursive-ai-oss/oneyaml-go) and [oneyaml-rust](https://github.com/recursive-ai-oss/oneyaml-rust), all measured by [oneyaml-kit](https://github.com/recursive-ai-oss/oneyaml-kit). ## Packages | Package | What it is | |---|---| | [`OneYaml`](https://www.nuget.org/packages/OneYaml) | The library: `YamlSerializer`, `YamlDocument`, patches, limits. Start here. | | [`OneYaml.Compat.YamlDotNet`](https://www.nuget.org/packages/OneYaml.Compat.YamlDotNet) | A subset of YamlDotNet's API on top of `OneYaml`: the serializer builders and the representation model, tested against YamlDotNet 18.1.0. Not covered: static contexts, type inspectors, node deserializers, the scanner and comment events; [what is covered and every difference](https://recursiveai.org/oneyaml/csharp/docs/compat-yamldotnet/). | | [`OneYaml.Compat.SharpYaml`](https://www.nuget.org/packages/OneYaml.Compat.SharpYaml) | A subset of SharpYaml's API on top of `OneYaml`: the serializer, its options and attributes, tested against SharpYaml 3.14.0. Not covered: custom converters, polymorphism, generated contexts, the `Model` and `Syntax` namespaces; [what is covered and every difference](https://recursiveai.org/oneyaml/csharp/docs/compat-sharpyaml/). | ## License Licensed under either of [Apache License, Version 2.0](https://github.com/recursive-ai-oss/oneyaml-csharp/tree/7818048c4be434333767c47d66176fc626d1da8e/LICENSE-APACHE) or [MIT license](https://github.com/recursive-ai-oss/oneyaml-csharp/tree/7818048c4be434333767c47d66176fc626d1da8e/LICENSE-MIT) at your option. # Serializer `YamlSerializer` reads YAML into .NET types and writes .NET values as YAML. Its shape is the shape of System.Text.Json: static `Serialize` and `Deserialize` methods, an immutable options object, naming policies, converters, attributes, and a source generator for trimmed and native AOT applications. If you know `JsonSerializer`, most of this page will look familiar; the sections below say where YAML differs. The samples on this page are complete programs. They assume implicit usings (the default for new projects), and the ones that use `JsonNamingPolicy` or the System.Text.Json attributes also need `using System.Text.Json;` or `using System.Text.Json.Serialization;`. ## Reading and writing ```csharp using OneYaml; var job = YamlSerializer.Deserialize(""" name: build image: dotnet/sdk:10.0 steps: - dotnet restore - dotnet test env: CI: "true" """)!; Console.WriteLine($"{job.Name}: {job.Steps.Count} steps"); // build: 2 steps job.Steps.Add("dotnet pack"); Console.Write(YamlSerializer.Serialize(job)); // name: build // image: dotnet/sdk:10.0 // steps: // - dotnet restore // - dotnet test // - dotnet pack // env: // CI: 'true' class Job { public string Name { get; set; } = ""; public string? Image { get; set; } public List Steps { get; set; } = []; public Dictionary Env { get; set; } = []; } ``` Things to know: - **Input and output.** `Deserialize` reads a `string`, a `ReadOnlySpan`, UTF-8 bytes (`ReadOnlySpan`; UTF-16 and UTF-32 are detected by a byte order mark or by their zero bytes), a `Stream` (also `DeserializeAsync`) or a `TextReader`. `Serialize` writes a `string`, UTF-8 bytes (`SerializeToUtf8Bytes`), a `Stream` (also `SerializeAsync`), a `TextWriter` or an `IBufferWriter`. - **One document.** `Deserialize` reads exactly one document; a stream that holds more is an error that names `DeserializeAll` ([Several documents](#several-documents)). - **What is written.** Block style, two spaces per level, and a line break at the end. A string that a YAML 1.1 reader would take for something else is quoted, so `"true"`, `"yes"` and `"007"` are written as `'true'`, `'yes'` and `'007'`, and the output reads back as the same values under YAML 1.2 and YAML 1.1. A string with line breaks is written as a literal block (`|`). Members whose value is `null` are written as `null` unless you change [`DefaultIgnoreCondition`](#options). - **What is read.** Plain scalars follow the YAML 1.2 Core schema: `yes` and `on` are strings, `0755` is 755, `0x1F` is 31 and `1_000` is a string. A quoted scalar is always a string, so `"5"` never binds to an `int`. Keys that match no member are skipped unless you set [`UnmappedMemberHandling`](#options). A key that appears twice in a mapping is an error. - **Records and constructors.** Records, constructor parameters, `init` accessors and `required` members bind without attributes, as in System.Text.Json. - **Dates.** A `DateTime` read from a timestamp with an offset is converted to the local time of the machine, as System.Text.Json does; use `DateTimeOffset` to keep the offset. ## Options `YamlSerializerOptions` uses `init` accessors and is read-only once it has been used. Create an instance once and reuse it: type metadata is built once per instance. To change a copy, use `new YamlSerializerOptions(existing) { ... }`. | Option | Default | What it does | |---|---|---| | `PropertyNamingPolicy` | `JsonNamingPolicy.CamelCase` | How member names become keys, for reading and writing. `null` keeps names as declared. | | `PropertyNameCaseInsensitive` | `false` | Match keys to members without regard to case, after the naming policy. | | `UnmappedMemberHandling` | `Skip` | `Disallow` makes a key that matches no member a binding error. | | `DefaultIgnoreCondition` | `Never` | `WhenWritingNull` or `WhenWritingDefault` leave members out of the output. | | `EnumNamingPolicy` | `null` | How enum members are named; `null` writes them as declared ([Enums](#enums)). | | `IndentSize` | `2` | Spaces per level when writing, 1 to 8. | | `Limits` | `YamlLimits.Default` | The bounds enforced while reading ([Limits](https://recursiveai.org/oneyaml/csharp/docs/limits/)). | | `MergeKeys` | `false` | Merge YAML 1.1 `<<` keys; when off, `<<` is an ordinary key. | | `SourceName` | `null` | A name, such as a file name, that starts every error message. | | `Converters` | empty | Custom converters, tried before the built-in ones ([Converters](#converters)). | | `TypeInfoResolver` | `null` | Where metadata comes from: a generated context, or reflection when `null` ([Source generation](#source-generation-and-native-aot)). | ```csharp var options = new YamlSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.KebabCaseLower, DefaultIgnoreCondition = YamlIgnoreCondition.WhenWritingNull, UnmappedMemberHandling = YamlUnmappedMemberHandling.Disallow, SourceName = "pool.yaml", }; Console.Write(YamlSerializer.Serialize(new Pool("web", MaxConnections: 100, Comment: null), options)); // pool-name: web // max-connections: 100 try { YamlSerializer.Deserialize("pool-name: web\nmax-conections: 100\n", options); } catch (YamlException e) { Console.WriteLine(e.Message); // pool.yaml: The key 'max-conections' is not a member of Pool. (line 2, column 1, path /max-conections) } record Pool(string PoolName, int? MaxConnections = null, string? Comment = null); ``` ## Attributes The YAML attributes are in the `OneYaml` namespace. Where a member has none, the System.Text.Json attribute of the same meaning is used, so a model written for `JsonSerializer` binds without change. When a member has both, the YAML attribute wins. | YAML attribute | System.Text.Json attribute also honoured | Effect | |---|---|---| | `[YamlPropertyName("key")]` | `[JsonPropertyName]` | The key of the member, instead of the naming policy's | | `[YamlIgnore]`, with an optional `Condition` | `[JsonIgnore]` | Never read or written, or left out when null or default | | `[YamlRequired]` (and the `required` keyword) | `[JsonRequired]` | A missing key is a binding error | | `[YamlPropertyOrder(n)]` | `[JsonPropertyOrder]` | The member's position when writing | | `[YamlConstructor]` | `[JsonConstructor]` | The constructor to bind through | | `[YamlExtensionData]` | `[JsonExtensionData]`, on `Dictionary` only | Collects keys no member takes ([Extension data](#extension-data)) | | `[YamlPolymorphic]`, `[YamlDerivedType]` | `[JsonPolymorphic]`, `[JsonDerivedType]` (key form) | [Polymorphism](#polymorphism) | | `[YamlEnumMemberName("name")]` | `[JsonStringEnumMemberName]` | The text of one enum member | | `[YamlConverter(typeof(C))]` | none: `[JsonConverter]` converters read JSON tokens | [Converters](#converters) | ```csharp var probe = YamlSerializer.Deserialize("path: /healthz\nperiod-seconds: 10\n")!; Console.WriteLine($"{probe.Path} every {probe.PeriodSeconds}s"); // /healthz every 10s try { YamlSerializer.Deserialize("period-seconds: 10\n"); } catch (YamlException e) { Console.WriteLine(e.Message); // The required key 'path' is missing. (line 1, column 1) } class Probe { [YamlRequired] public string Path { get; set; } = ""; [YamlPropertyName("period-seconds")] public int PeriodSeconds { get; set; } = 30; [JsonIgnore] // System.Text.Json attributes are honoured where no YAML attribute is present public string? Cached { get; set; } } ``` ## Enums Enum members are written as declared; the property naming policy does not apply to them. Set `EnumNamingPolicy` to rename them all, or `[YamlEnumMemberName]` to rename one. Reading accepts the declared names and the attributed names, in any letter case. ```csharp Console.Write(YamlSerializer.Serialize(new Container("web", PullPolicy.IfNotPresent))); // name: web // pull: IfNotPresent var options = new YamlSerializerOptions { EnumNamingPolicy = JsonNamingPolicy.CamelCase }; Console.Write(YamlSerializer.Serialize(new Container("web", PullPolicy.IfNotPresent), options)); // name: web // pull: ifNotPresent Console.WriteLine(YamlSerializer.Deserialize("name: web\npull: ifnotpresent\n")!.Pull); // IfNotPresent enum PullPolicy { Always, IfNotPresent, Never } record Container(string Name, PullPolicy Pull); ``` ## Converters Derive from `YamlConverter` to read and write a type yourself, as you would derive from `JsonConverter`. `Read` gets a `Utf8YamlReader` on the first token of the value and must leave it on the last one; `Write` writes exactly one value to a `Utf8YamlEmitter`. Throw `reader.CreateException(message)` to report bad input: the exception carries the position and path of the value. A `new YamlException(message)` thrown from a converter is completed the same way and reported as `YamlErrorCategory.Binding`. ```csharp var options = new YamlSerializerOptions { Converters = { new VersionConverter() } }; var release = YamlSerializer.Deserialize("name: app\nversion: 2.1.0\n", options)!; Console.WriteLine(release.Version.Minor); // 1 try { YamlSerializer.Deserialize("name: app\nversion: two\n", options); } catch (YamlException e) { Console.WriteLine(e.Message); // 'two' is not a version. (line 2, column 10, path /version) } record Release(string Name, Version Version); sealed class VersionConverter : YamlConverter { public override Version Read(ref Utf8YamlReader reader, Type typeToConvert, YamlSerializerOptions options) { string text = reader.GetString(); return Version.TryParse(text, out Version? version) ? version : throw reader.CreateException($"'{text}' is not a version."); } public override void Write(Utf8YamlEmitter writer, Version value, YamlSerializerOptions options) => writer.WriteStringValue(value.ToString()); } ``` A converter can also be named by `[YamlConverter(typeof(VersionConverter))]` on a type or a member. When several apply, a converter on the member wins over one in `Converters`, which wins over one on the type; this holds for reflection and for generated metadata. On a generic type the attribute may name an open generic converter, such as `typeof(BoxConverter<>)`, and `YamlConverterFactory` makes converters on demand. `HandleNull` says whether `Read` is called for a null value. [Reader and emitter](https://recursiveai.org/oneyaml/csharp/docs/reader-emitter/) shows a converter that reads a mapping token by token. ## Polymorphism Mark a base type with `[YamlPolymorphic]` and list each derived type with `[YamlDerivedType(typeof(D), "name")]`. Only listed types are ever created, so the text cannot choose an arbitrary .NET type. By default the discriminator is the key `$type`; it may be anywhere in the mapping, and when it is not the first key the reader looks ahead for it, which counts against the alias budget of the [limits](https://recursiveai.org/oneyaml/csharp/docs/limits/). Set `TypeDiscriminatorPropertyName` to use another key. ```csharp var shapes = YamlSerializer.Deserialize>(""" - $type: circle radius: 2.0 - side: 3.0 $type: square """)!; Console.WriteLine(string.Join(", ", shapes)); // Circle { Radius = 2 }, Square { Side = 3 } Console.Write(YamlSerializer.Serialize>([new Square { Side = 1.5 }])); // - $type: square // side: 1.5 [YamlPolymorphic] [YamlDerivedType(typeof(Circle), "circle")] [YamlDerivedType(typeof(Square), "square")] abstract record Shape; record Circle : Shape { public double Radius { get; init; } } record Square : Shape { public double Side { get; init; } } ``` A discriminator that names no listed type is a binding error. YAML can also say the type with a tag, which suits configuration formats that use tags such as `!run`. With `DiscriminatorKind = YamlDiscriminatorKind.Tag`, the derived types may be mappings, sequences or scalars: ```csharp var steps = YamlSerializer.Deserialize>(""" - !run make test - !copy {from: bin/app, to: /opt/app} """)!; Console.WriteLine(string.Join(", ", steps)); // Run { Command = make test }, Copy { From = bin/app, To = /opt/app } Console.Write(YamlSerializer.Serialize(steps)); // - !run make test // - !copy // from: bin/app // to: /opt/app [YamlPolymorphic(DiscriminatorKind = YamlDiscriminatorKind.Tag)] [YamlDerivedType(typeof(Run), "run")] [YamlDerivedType(typeof(Copy), "copy")] abstract record Step; [YamlConverter(typeof(RunConverter))] // a run step is written as a plain string record Run(string Command) : Step; record Copy(string From, string To) : Step; sealed class RunConverter : YamlConverter { public override Run Read(ref Utf8YamlReader reader, Type typeToConvert, YamlSerializerOptions options) => new(reader.GetString()); public override void Write(Utf8YamlEmitter writer, Run value, YamlSerializerOptions options) => writer.WriteStringValue(value.Command); } ``` `[JsonPolymorphic]` and `[JsonDerivedType]` are honoured too, always with a key discriminator. ## Extension data A `Dictionary` or `IDictionary` member marked `[YamlExtensionData]` collects the keys no other member takes, and they are written back after the other members. The values have the untyped shapes of the next section. ```csharp var plugin = YamlSerializer.Deserialize(""" name: lint level: strict paths: [src, tests] """)!; Console.WriteLine(string.Join(", ", plugin.Settings.Keys)); // level, paths Console.Write(YamlSerializer.Serialize(plugin)); // name: lint // level: strict // paths: // - src // - tests class Plugin { public string Name { get; set; } = ""; [YamlExtensionData] public Dictionary Settings { get; set; } = []; } ``` ## Untyped values `Deserialize` gives a fixed set of shapes: `Dictionary` (in document order), `List`, `string`, `long`, `BigInteger` for integers that do not fit in a `long` (never rounded), `double`, `bool` and `null`. ```csharp var root = (Dictionary)YamlSerializer.Deserialize(""" port: 8080 ratio: 0.5 enabled: yes id: 123456789012345678901234567890 80: http tags: [a, b] """)!; foreach (var (key, value) in root) { Console.WriteLine($"{key}: {value?.GetType().Name}"); // port: Int64 // ratio: Double // enabled: String // id: BigInteger // 80: String // tags: List`1 } ``` - A key is its source text, so `80: http` has the key `"80"`. Two keys with the same text are a duplicate-key error. A key that is a mapping or a sequence cannot be a dictionary key; the error points you to `YamlDocument`, which can read it. - Tags are dropped wherever the target is `object`: `!Sub x` reads as the string `x`. Timestamps stay strings. To see tags, use a converter (`Utf8YamlReader.Tag`) or a `YamlDocument`, which keeps them. - An alias gives the same instance as its anchor, and every alias counts against the alias budget of the limits. An alias inside the node its own anchor names (`&a [*a]`) is a cycle and a `Syntax` error. ## Several documents A YAML stream can hold several documents separated by `---`. `DeserializeAll` reads them all, `DeserializeAllAsync` returns them from a stream as an `IAsyncEnumerable`, and `SerializeAll` writes several. ```csharp IReadOnlyList pools = YamlSerializer.DeserializeAll(""" pool-name: web --- pool-name: jobs max-connections: 10 """, new YamlSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.KebabCaseLower }); Console.WriteLine(pools.Count); // 2 Console.Write(YamlSerializer.SerializeAll(["a", "b"])); // a // --- // b ``` ## Errors Every failure caused by the input is a `YamlException`. Its `Message` stands on its own: it says what failed and where, with the `SourceName` first when you set one. ```csharp try { YamlSerializer.Deserialize("poolName: web\nmaxConnections: many\n", new YamlSerializerOptions { SourceName = "pools.yaml" }); } catch (YamlException e) { Console.WriteLine(e.Category); // Binding Console.WriteLine($"{e.Line}:{e.Column}"); // 2:17 Console.WriteLine(e.Path); // /maxConnections Console.WriteLine(e.Message); // pools.yaml: The value 'many' cannot be read as a 32-bit integer. (line 2, column 17, path /maxConnections) } ``` `Category` is the stable part to test in code: `Syntax`, `Encoding`, `Limit`, `DuplicateKey`, `UnresolvedTag` and `Binding` come from reading, the others from [editing](https://recursiveai.org/oneyaml/csharp/docs/editing/) and [patches](https://recursiveai.org/oneyaml/csharp/docs/patches/). `None` is what the public `YamlException` constructors give; the serializer reports such an exception from a converter as `Binding`. `Line` and `Column` are 1-based and count characters; `Offset` is the byte offset in the UTF-8 text. `Path` names the value that failed to bind, in RFC 6901 pointer form. The text of a message is written for people and may change between releases. ## Source generation and native AOT The overloads that take `YamlSerializerOptions` use reflection, and are marked `[RequiresUnreferencedCode]` and `[RequiresDynamicCode]` for that reason. For a trimmed or native AOT application, declare a context and pass its generated metadata instead: ```csharp Settings settings = YamlSerializer.Deserialize("retries: 3\nendpoint: https://example.com\n", AppYamlContext.Default.Settings)!; Console.WriteLine(settings.Retries); // 3 string yaml = YamlSerializer.Serialize(settings with { Retries = 5 }, AppYamlContext.Default.Settings); Console.Write(yaml); // retries: 5 // endpoint: https://example.com record Settings(int Retries, Uri Endpoint); [YamlSerializable(typeof(Settings))] partial class AppYamlContext : YamlSerializerContext; ``` - The generator ships in the `OneYaml` package; there is nothing else to install. It writes a `YamlTypeInfo` property on the context for each `[YamlSerializable]` type and for every type those reach. - Generated and reflection metadata bind the same way: the attributes, converters, polymorphism and extension data above work with both. - For options other than the defaults, create the context with them, `new AppYamlContext(new YamlSerializerOptions { ... })`, or set `TypeInfoResolver = AppYamlContext.Default` on options you already pass around. - In an application published with `PublishTrimmed` or `PublishAot`, reflection-based binding is off by default, because it cannot work reliably once the trimmer has removed members. Set the MSBuild property `YamlSerializerIsReflectionEnabledByDefault` to `true` to keep it. - Mistakes in a context are reported at build time as diagnostics `OY0001` to `OY0010`, such as a context that is not `partial` or a derived type that is not listed correctly. - `YamlDocument` has generated-metadata overloads too: `Set(path, value, typeInfo)`, `Get(path, typeInfo)` and `YamlNode.Deserialize(typeInfo)` ([Editing](https://recursiveai.org/oneyaml/csharp/docs/editing/)). # Editing `YamlDocument` holds a whole YAML stream with its text, comments and layout. Writing it back unedited gives the input byte for byte, and an edit changes only the text of the value it touches. Use it for files that people also edit by hand, where the diff of a change must hold only that change. The samples on this page are complete programs that assume implicit usings (the default for new projects); the first shows the `using OneYaml;` line the others leave out. ## Indexers `doc.Root` is the root node of the first document. A `YamlNode` reads like `JsonNode`: `node["key"]` for a mapping, `node[0]` for a sequence, and assignment to change a value. ```csharp using OneYaml; using var doc = YamlDocument.Parse(""" # Deployment for the web tier spec: replicas: 2 # raised by the release job image: web:1.4 ports: [80, 443] """); YamlNode spec = doc.Root["spec"]; spec["replicas"] = 3; // replaces the value, keeps the comment spec["paused"] = false; // a new key goes at the end of its mapping spec["ports"][2] = 8443; // an index equal to the count appends spec["image"].Remove(); Console.WriteLine(doc); // # Deployment for the web tier // spec: // replicas: 3 # raised by the release job // ports: [80, 443, 8443] // paused: false ``` ### What an edit keeps, and what it does not - **Untouched text is kept byte for byte:** comments, blank lines, indentation, key order, the quoting of other values, line endings (`\n` or `\r\n`). A comment on the same line as a replaced scalar stays after the new value. - **Removing an entry** removes its line, with the comment on that line and the comment lines just above it. - **Replacing a mapping or a sequence** replaces its whole text, and the comments inside it go with it. Change the leaves instead when the comments matter. - **Comments do not move with values.** A value moved by a patch `move` is written in the style of its new place, without the comments of its old one. - **There is no comment API.** Comments are kept, not modelled: you cannot read, add or move them. - **Encoding.** `ToString()` gives text. `ToBytes()` and `WriteTo(...)` give the encoding and byte order mark the input had; use them to write the file back ([Input, output and lifetime](#input-output-and-lifetime)). - **Plain or quoted** is judged by the YAML 1.2 Core schema, so a new string `yes` is written plain. If a YAML 1.1 reader also reads the file, set `QuoteYaml11Lookalikes` ([How a new value is written](#how-a-new-value-is-written)). - **Values you can assign.** `string`, `bool`, the integer types, `double`, `decimal` and `null` convert to a `YamlNode` implicitly. For anything else, `YamlNode.Create(value)` or `node.Set(value)` takes a dictionary, a sequence, another node, or any value the serializer can write. - **A view follows its node.** A `YamlNode` stays valid across edits made through it or anywhere else in the document, as a `JsonNode` does. A view of a node that was removed, or that was inside a value that was replaced, throws `InvalidOperationException`. Any use after the document is disposed throws `ObjectDisposedException`. - **Misses.** Reading a key that is not there throws `KeyNotFoundException`; use `TryGetValue` or `ContainsKey` to look without throwing. A sequence index outside the items throws `ArgumentOutOfRangeException`, and using a key on a scalar or a sequence throws `InvalidOperationException`. ## Pointers Every edit can also be made with an RFC 6901 JSON Pointer. A token names a mapping key by its text, so `/80` reaches the key `80`; a sequence item by its index; and `-` the place after the last item. In a key, `~1` stands for `/` and `~0` for `~`. ```csharp using var doc = YamlDocument.Parse(""" metadata: annotations: example.com/owner: web-team spec: containers: - name: web image: web:1.4 """); doc.Set("/spec/containers/0/image", "web:1.5"); doc.Set("/metadata/annotations/example.com~1owner", "platform"); // ~1 stands for '/' doc.Add("/spec/containers/-", new Dictionary { ["name"] = "proxy", ["image"] = "envoy:1.30" }); Console.WriteLine(doc.Get("/spec/containers/1/name")); // proxy Console.WriteLine(doc.Contains("/spec/volumes")); // False if (doc.TryGet("/spec/containers/0/image", out string? image)) { Console.WriteLine(image); // web:1.5 } Console.WriteLine(doc); // metadata: // annotations: // example.com/owner: platform // spec: // containers: // - name: web // image: web:1.5 // - name: proxy // image: envoy:1.30 ``` | Method | What it does | |---|---| | `Set(path, value)` | Replaces the value, or adds a missing mapping key at the end of its mapping; `-` appends to a sequence | | `Add(path, value)` | RFC 6902 `add`: a sequence index inserts before the item there | | `Remove(path)` | Removes the entry or item | | `Get(path)`, `TryGet(path, out value)` | Reads a scalar as a .NET value | | `GetNode(path)` | Returns the node, to read or edit through its view | | `Contains(path)` | Tells whether the pointer names a value | A pointer that names nothing is a `YamlException` with category `PatchPath`, the category every pointer miss shares. `Set` and `Add` take any value the serializer can write: through reflection with the `Set(path, value, options)` overload, or through generated metadata with `Set(path, value, typeInfo)` ([Source generation](https://recursiveai.org/oneyaml/csharp/docs/serializer/#source-generation-and-native-aot)). `Get(path, typeInfo)` and `YamlNode.Deserialize()` read a whole subtree into a type. ## How a new value is written A replaced scalar keeps its style when that style can hold the new value. When it cannot, the value is written in the nearest style that can: a plain scalar becomes single-quoted, then double-quoted; a single-quoted one becomes double-quoted. A block scalar keeps its `|` or `>` and gets the chomping indicator the new text needs. The value read back is always exactly the value you set. ```csharp using var doc = YamlDocument.Parse(""" plain: one single: 'one' double: "one" literal: | one """); doc.Set("/plain", "two words"); // still plain doc.Set("/single", "it's"); // still single-quoted, the quote doubled doc.Set("/double", "tab\there"); // still double-quoted, with an escape doc.Set("/literal", "no final break"); // still literal, with the strip indicator Console.Write(doc); // plain: two words // single: 'it''s' // double: "tab\there" // literal: |- // no final break doc.Set("/plain", "a: b"); // plain cannot hold ': ', so it is quoted doc.Set("/single", "two\nlines\n"); // single quotes cannot keep the break, so double quotes Console.WriteLine(doc.Root["plain"].Style); // SingleQuoted Console.WriteLine(doc.Root["single"].Style); // DoubleQuoted ``` A new key or item is written like its siblings: in a flow collection (`[...]`, `{...}`) as flow, in a block collection as block, at the same indentation, with the line breaks the file already uses. A new mapping or sequence value is written in block style unless it goes inside a flow collection. Whether a new string can be plain is judged by the YAML 1.2 Core schema, the schema this library reads with: `yes` stays plain because it is a string under YAML 1.2, while `1.10` is quoted because it would read as a number. If a YAML 1.1 reader also reads the file, set `QuoteYaml11Lookalikes`, and such strings are quoted as `YamlSerializer` quotes them. ```csharp const string Src = "enabled: x\nversion: x\n"; using var core = YamlDocument.Parse(Src); core.Set("/enabled", "yes"); // a string under YAML 1.2, so it stays plain core.Set("/version", "1.10"); // a number under YAML 1.2, so it is quoted Console.Write(core); // enabled: yes // version: '1.10' using var both = YamlDocument.Parse(Src, new YamlDocumentOptions { QuoteYaml11Lookalikes = true }); both.Set("/enabled", "yes"); // quoted, so a YAML 1.1 reader also sees a string Console.WriteLine(both.Root["enabled"].Style); // SingleQuoted ``` ## Anchors and aliases Reading follows aliases: `Get`, the indexers and `Deserialize` see the anchored value. Writing through an alias is refused, because it would change every place that shares the anchor. Replacing the alias itself, or the anchored original, is fine. An alias means the most recent definition of its name that is written before it and already complete. An alias inside the node its own anchor names (`&a [*a]`) would be a cycle, so opening such a document is a `Syntax` error rather than a silent use of an older `&a`. ```csharp using var doc = YamlDocument.Parse(""" defaults: &defaults cpu: 2 prod: *defaults """); Console.WriteLine(doc.Get("/prod/cpu")); // 2 try { doc.Set("/prod/cpu", 4); // would change 'defaults' too } catch (YamlException e) when (e.Category == YamlErrorCategory.RefusedAlias) { Console.WriteLine(e.Message); // The edit would write through an alias and change every place that shares the anchored node. } doc.Set("/prod", new Dictionary { ["cpu"] = 4 }); // replacing the alias itself is fine Console.WriteLine(doc); // defaults: &defaults // cpu: 2 // prod: // cpu: 4 ``` Removing a node whose anchor is still used by an alias is refused too. `YamlNode.Anchor`, `Tag` (resolved against `%TAG` directives) and `RawTag` (as written) tell you what a node carries, and a replaced scalar keeps its tag. A view reached through an alias is a read-only snapshot that is valid until the next edit; take it again from the root after editing. Reading through aliases counts against the alias budget of the document's [limits](https://recursiveai.org/oneyaml/csharp/docs/limits/). ## Reading while you edit ```csharp using var doc = YamlDocument.Parse(""" name: web replicas: 2 labels: {tier: frontend, team: core} """); YamlNode root = doc.Root; Console.WriteLine(root["replicas"].GetValue() + 1); // 3 Console.WriteLine(root["labels"].Style); // Flow foreach (var (key, value) in root["labels"].Entries) { Console.WriteLine($"{key.GetValue()} = {value.GetValue()}"); // tier = frontend // team = core } if (!root.TryGetValue("owner", out YamlNode? owner)) { Console.WriteLine("no owner"); // no owner } var service = root.Deserialize()!; Console.WriteLine($"{service.Name}: {service.Labels.Count} labels"); // web: 2 labels record Service(string Name, int Replicas, Dictionary Labels); ``` `Kind`, `Style`, `Count`, `Position` (line, column and byte offset), `IsNull`, `Items` and `Entries` describe a node; `GetValue` and `TryGetValue` read a scalar. `Deserialize` binds a node as `YamlSerializer` would bind the same text, with the document's limits and source name. Unlike the serializer, a document keeps tags and accepts keys that are mappings or sequences. ## Repeated keys A key that appears twice in a mapping is a `DuplicateKey` error when the document is parsed. To edit such a file anyway, set `AllowDuplicateKeys`; a pointer or indexer that then matches more than one key is refused with `RefusedAmbiguous`. Keys are compared after Core schema resolution, so `1` and `0x1` are the same key, but `1` and `"1"` are not. ## Several documents A stream can hold several documents. `Documents` lists their roots, `Root` is the first one, and every pointer method and patch takes a `document` index, 0 by default. Output always writes the whole stream. ```csharp using var doc = YamlDocument.Parse(""" kind: Service name: web --- kind: Deployment name: web replicas: 2 """); Console.WriteLine(doc.Documents.Count); // 2 doc.Set("/replicas", 3, document: 1); doc.Documents[0]["name"] = "web-svc"; Console.WriteLine(doc); // kind: Service // name: web-svc // --- // kind: Deployment // name: web // replicas: 3 ``` A stream with no document (empty, or only comments) has `Documents.Count == 0`; `Root` then reads as null, and the first write creates the document. ## Recipe: update a Kubernetes manifest A deploy job changes a manifest that people also maintain by hand. The file holds a Service and a Deployment; the job raises the replica count, moves to a new image, turns off coloured logs and drops an environment variable nobody uses any more. The input stands for a file that came from outside the program, so it is read with `YamlLimits.Untrusted` ([Limits](https://recursiveai.org/oneyaml/csharp/docs/limits/)). Kubernetes tools read manifests with a YAML 1.1 parser, where a plain `no` is the boolean false, so `QuoteYaml11Lookalikes` is on. The file is written back with `WriteTo`, which keeps its encoding. ```csharp byte[] input = """ # Service for the web tier apiVersion: v1 kind: Service metadata: name: web --- apiVersion: apps/v1 kind: Deployment metadata: name: web spec: replicas: 2 # scaled by the release job template: spec: containers: - name: web image: web:1.4 env: # left over from the old proxy - name: PROXY_URL value: http://proxy:3128 - name: LOG_COLOR value: auto """u8.ToArray(); // in a real job: File.ReadAllBytes(path) var options = new YamlDocumentOptions { Limits = YamlLimits.Untrusted, // also bounds input size, scalar size and document count QuoteYaml11Lookalikes = true, // so a YAML 1.1 reader sees 'no' as a string }; using var doc = YamlDocument.Parse(input, options); // Find the Deployment by its kind, not by its place in the file. int deploy = Enumerable.Range(0, doc.Documents.Count).First(i => doc.Get("/kind", i) == "Deployment"); const string Web = "/spec/template/spec/containers/0"; doc.Set("/spec/replicas", 3, deploy); doc.Set($"{Web}/image", "web:1.5", deploy); doc.Set($"{Web}/env/1/value", "no", deploy); doc.Remove($"{Web}/env/0", deploy); // the comment line above it goes too using var file = new MemoryStream(); // in a real job: File.Create(path) doc.WriteTo(file); // in the encoding the input had file.Position = 0; Console.Write(new StreamReader(file).ReadToEnd()); // # Service for the web tier // apiVersion: v1 // kind: Service // metadata: // name: web // --- // apiVersion: apps/v1 // kind: Deployment // metadata: // name: web // spec: // replicas: 3 # scaled by the release job // template: // spec: // containers: // - name: web // image: web:1.5 // env: // - name: LOG_COLOR // value: 'no' ``` The diff of the file holds only the job's changes: the replica count and the image, each in place with its comment kept, the quoted `'no'`, and the removed variable with the comment line that described it. The Service is untouched. Without `QuoteYaml11Lookalikes` the value would be written as a plain `no`, which is a string under YAML 1.2 but false for a YAML 1.1 reader. To change many files, catch `YamlException` for each one: a file over the limits, or one where the Deployment has no such path, then fails on its own and nothing is written for it. ## Errors A failed edit throws and leaves the document exactly as it was. If even putting the document back fails, for example because memory runs out while it is restored, the edit's own exception still reaches you, and from then on the document refuses every use with `InvalidOperationException`; `Dispose` still works. Problems with the YAML, the pointer or the value are a `YamlException` with a category: `PatchPath` (the pointer names nothing), `RefusedAlias`, `RefusedAmbiguous`, or `Limit`. Mistakes in how the API is used are the usual .NET exceptions listed above (`KeyNotFoundException`, `ArgumentOutOfRangeException`, `InvalidOperationException`, `ObjectDisposedException`), and a value with no YAML form is an `ArgumentException`. ## Input, output and lifetime - `Parse` takes a `string`, a `ReadOnlySpan`, bytes in UTF-8, UTF-16 or UTF-32 (detected from a byte order mark or the first characters), or a `Stream` (also `ParseAsync`). A seekable stream longer than `MaxInputBytes` is refused before it is read. - `ToString()` gives the text; `ToUtf8Bytes()` UTF-8; `ToBytes()`, `WriteTo(...)` and `WriteToAsync(...)` the encoding and byte order mark the input had. - `WriteTo(IBufferWriter)` and `WriteTo(Stream)` write into a buffer you own and can reuse, such as an `ArrayBufferWriter` cleared with `ResetWrittenCount()` between calls. For a document read as UTF-8 they copy its text straight into that buffer, while `ToUtf8Bytes()` and `ToBytes()` allocate a new array on every call; this matters when you write large documents again and again. A UTF-16 or UTF-32 document is converted first, so writing it allocates either way, and `WriteToAsync` copies the text into a new array before it awaits the write. - `YamlDocumentOptions` sets `Limits`, `AllowDuplicateKeys`, `QuoteYaml11Lookalikes` and `SourceName` (the name that starts every error message). - A document holds pooled memory; dispose it when you are done. It is not thread-safe. - To apply a JSON Patch or a merge patch, see [Patches](https://recursiveai.org/oneyaml/csharp/docs/patches/). # Patches `YamlDocument` applies JSON Patch operations ([RFC 6902](https://www.rfc-editor.org/rfc/rfc6902)) with YAML value equality for `test`, and JSON Merge Patch ([RFC 7396](https://www.rfc-editor.org/rfc/rfc7396)), to a YAML file and keeps its layout: every value the patch does not touch keeps its text, comments included, and every value it writes is placed and styled as an [edit](https://recursiveai.org/oneyaml/csharp/docs/editing/#how-a-new-value-is-written) would be. Paths are RFC 6901 pointers, read as in [Pointers](https://recursiveai.org/oneyaml/csharp/docs/editing/#pointers). One rule differs from RFC 6902 on purpose: `test` compares YAML values, so `1` and `1.0` are different ([Differences from RFC 6902](#differences-from-rfc-6902)). The samples on this page are complete programs that assume implicit usings (the default for new projects); the first shows the `using OneYaml;` line the others leave out. ## JSON Patch ```csharp using OneYaml; using var doc = YamlDocument.Parse(""" name: web # the public site replicas: 2 labels: tier: frontend """); doc.ApplyPatch(""" [ { "op": "test", "path": "/name", "value": "web" }, { "op": "replace", "path": "/replicas", "value": 3 }, { "op": "add", "path": "/labels/team", "value": "core" } ] """); Console.WriteLine(doc); // name: web # the public site // replicas: 3 // labels: // tier: frontend // team: core ``` `ApplyPatch` takes the patch as JSON text, UTF-8 bytes, a `JsonNode`, or a `YamlPatch` built in code. All six operations are supported: `add`, `remove`, `replace`, `move`, `copy` and `test`. A value that is moved or copied is written in the style of its new place, so a comment on the line of a moved value does not move with it. > **`test` compares YAML values, not JSON values.** An integer and a float are different values in YAML 1.2, so a `test` for `1` fails on `1.0` in the file, where RFC 6902 would pass it. This is on purpose: the patch checks the value a YAML reader sees. A patch made for a JSON form of the data may need its `test` values changed; see [Differences from RFC 6902](#differences-from-rfc-6902). ```csharp using var doc = YamlDocument.Parse("timeout: 1.0\n"); try { doc.ApplyPatch("""[{ "op": "test", "path": "/timeout", "value": 1 }]"""); } catch (YamlException e) when (e.Category == YamlErrorCategory.PatchTest) { Console.WriteLine(e.Message); // The value at '/timeout' is not the value the test expects. } doc.ApplyPatch("""[{ "op": "test", "path": "/timeout", "value": 1.0 }]"""); // passes ``` ## A patch is atomic The operations run in order. If one fails, the document is left exactly as it was before the patch, and the `YamlException` says which operation failed in `OperationIndex` (0-based). ```csharp const string Src = "name: web\nreplicas: 2\n"; using var doc = YamlDocument.Parse(Src); try { doc.ApplyPatch(""" [ { "op": "replace", "path": "/replicas", "value": 5 }, { "op": "test", "path": "/name", "value": "api" } ] """); } catch (YamlException e) { Console.WriteLine($"{e.Category} at operation {e.OperationIndex}"); // PatchTest at operation 1 } Console.WriteLine(doc.ToString() == Src); // True ``` | Category | Meaning | |---|---| | `PatchInvalid` | The patch is not valid JSON, or an operation is malformed (an unknown `op`, a missing `value`) | | `PatchPath` | A `path` or `from` names nothing, or the `document` index is outside the stream | | `PatchTest` | A `test` found a different value | | `RefusedAlias` | The operation would write through an alias ([Anchors and aliases](https://recursiveai.org/oneyaml/csharp/docs/editing/#anchors-and-aliases)) | | `RefusedAmbiguous` | A pointer matches more than one key (with `AllowDuplicateKeys`) | | `Limit` | The patch or the values it reads exceed the document's [limits](https://recursiveai.org/oneyaml/csharp/docs/limits/) | ## Differences from RFC 6902 The six operations and the order they run in follow RFC 6902, and paths follow RFC 6901. `test` compares the value in the document with the patch's value by the YAML data model under the Core schema, after following aliases, not as JSON values: | | RFC 6902 | `ApplyPatch` | |---|---|---| | `test` on numbers | numbers are equal when they are numerically equal: `1` equals `1.0` | an integer and a float are different values: `1` does not equal `1.0`. Values of one type compare by value: `0x1F` equals `31`, and `1e2` equals `100.0` | | `test` on mapping keys | object member names are strings | keys compare as YAML values: a plain key `80` is an integer and does not match the member `"80"` of a test value; a quoted key `"80"` does | | `test` on strings | JSON strings | the scalar as the Core schema reads it: plain `yes` equals `"yes"`, and quoted `'1'` is a string, so it does not equal `1` | | What the patch applies to | one JSON document | one document of a YAML stream, the first unless you pass `document: n` ([Several documents](#several-documents)) | Pointers are not affected: a token names a key by its text, so `/80` reaches the integer key `80` as it would reach the member `"80"` of a JSON object. **A patch made for a JSON form of the same data**, for example by a tool that converted the file to JSON and diffed it, applies the same way except for its `test` operations on floats and on mappings with keys that are not strings. Check those first: a JSON writer may write a whole float as `1`, and that `test` then fails on the `1.0` in the file. The values such a patch writes are read as the JSON shows them, so a float written as `1` goes into the file as the integer `1`. ## Building a patch in code `YamlPatch` builds the same operations with typed values. The values are converted when they are added, so one patch can be applied to any number of documents. `YamlPatch.Parse` reads one from JSON, and the patch is a read-only list of its operations. ```csharp var patch = new YamlPatch() .Test("/name", "web") .Replace("/replicas", 3) .Add("/labels/tier", "frontend"); using var doc = YamlDocument.Parse("name: web\nreplicas: 2\nlabels: {}\n"); doc.ApplyPatch(patch); Console.Write(doc); // name: web // replicas: 3 // labels: {tier: frontend} ``` `Add`, `Replace` and `Test` take plain values (null, strings, booleans, numbers, dictionaries and sequences of these), or any type through the reflection overload with `YamlSerializerOptions` or the generated-metadata overload with a `YamlTypeInfo`. ## Merge patches `ApplyMergePatch` applies an RFC 7396 merge patch: an object merges member by member, `null` removes a member, and any other value, an array included, replaces what is there. Removals in a mapping run after its other members. Merging an object into an alias is refused, as any write through an alias is. ```csharp using var doc = YamlDocument.Parse(""" name: web labels: tier: frontend # used by the load balancer legacy: true """); doc.ApplyMergePatch("""{ "labels": { "legacy": null, "team": "core" }, "replicas": 3 }"""); Console.WriteLine(doc); // name: web // labels: // tier: frontend # used by the load balancer // team: core // replicas: 3 ``` A merge patch is atomic too, and takes JSON text, UTF-8 bytes or a `JsonNode`. ## Several documents A patch applies to one document of the stream, the first by default; pass `document: n` for another. An index outside the stream is a `PatchPath` error, as for any other path the patch cannot reach. # Limits YAML can describe far more work than its size suggests: an alias can stand for a whole subtree, so a few hundred bytes can expand to a billion values, and deep nesting can exhaust the stack. `YamlLimits` bounds that work. Exceeding a limit throws a `YamlException` with category `Limit`, and the message names the limit and its value. The samples on this page are complete programs that assume implicit usings (the default for new projects); the first shows the `using OneYaml;` line the others leave out. ## The presets | Limit | `YamlLimits.Default` | `YamlLimits.Untrusted` | |---|---|---| | `MaxDepth`: nesting of collections | 1,000 | 128 | | Alias expansion, nodes (`AliasNodeFactor`, `AliasNodeFloor`) | 10 × the document's own nodes, at least 1,000,000 | 2 × the document's own nodes, at least 100,000 | | Alias expansion, bytes (`AliasByteFactor`, `AliasByteFloor`) | 10 × the input size, at least 64 MiB | 2 × the input size, at least 8 MiB | | `MaxIntegerDigits`: digits of an integer read as a number of any size | 4,300 | 4,300 | | `MaxInputBytes` | none | 64 MiB | | `MaxScalarBytes` | none | 16 MiB | | `MaxDocuments`: documents per stream | none | 10,000 | `Default` applies whenever you pass no limits. It bounds the work that grows faster than the input (depth, alias expansion, long integers) and is meant never to get in the way of a real file. It sets no cap on input size, scalar size or the number of documents: those are capped only when you set `MaxInputBytes`, `MaxScalarBytes` and `MaxDocuments`, which `Untrusted` does, and `Untrusted` also tightens the default bounds. `YamlLimits.None` removes every bound you can set, alias expansion included; use it only for input you fully trust. Under any limits, a stream or reader too large for one buffer is still refused ([Input size](#input-size)). Integers that fit in 64 bits are not affected by `MaxIntegerDigits`; the bound exists because converting a very long integer to a `BigInteger` takes more than linear time. ## Input you do not control Pass `YamlLimits.Untrusted` for anything that comes from outside your program: uploads, request bodies, files from users or other systems. ```csharp using OneYaml; // Nine levels of ten aliases each: a few hundred bytes that expand to a billion strings. var bomb = new StringBuilder("a: &a [x, x, x, x, x, x, x, x, x, x]\n"); for (char c = 'b'; c <= 'i'; c++) { string prev = $"*{(char)(c - 1)}"; bomb.Append($"{c}: &{c} [{string.Join(", ", Enumerable.Repeat(prev, 10))}]\n"); } var options = new YamlSerializerOptions { Limits = YamlLimits.Untrusted }; try { YamlSerializer.Deserialize(bomb.ToString(), options); } catch (YamlException e) when (e.Category == YamlErrorCategory.Limit) { Console.WriteLine(e.Message); // Expanding the aliases exceeds the budget set by YamlLimits (AliasNodeFactor 2, AliasNodeFloor 100000, AliasByteFactor 2, AliasByteFloor 8388608). (line 5, column 36) } ``` (`StringBuilder` needs `using System.Text;`.) The same limits go to a document through `YamlDocumentOptions`, and they hold for everything that follows aliases there: the indexers, `Get`, `Deserialize`, and the `test` and `copy` operations of a patch. ```csharp string deep = new string('[', 200) + new string(']', 200); try { YamlSerializer.Deserialize(deep, new YamlSerializerOptions { Limits = YamlLimits.Untrusted }); } catch (YamlException e) { Console.WriteLine(e.Message); // The nesting depth is larger than the limit MaxDepth (128). (line 1, column 129) } ``` The depth is checked before any binder or converter frame runs, so the depth of the call stack is bounded too. A stack overflow ends a .NET process and cannot be caught, which is why the default depth is 1,000 and not higher. ## Changing one bound `YamlLimits` is a record: start from a preset and change what your input needs with `with`. ```csharp // Start from the preset and change only what your input needs. YamlLimits limits = YamlLimits.Untrusted with { MaxInputBytes = 1024 * 1024, MaxDocuments = 1 }; try { using var doc = YamlDocument.Parse(new string('#', 2 * 1024 * 1024), new YamlDocumentOptions { Limits = limits }); } catch (YamlException e) { Console.WriteLine(e.Category); // Limit } ``` ## Input size `MaxInputBytes` is checked against the input as given and again against its UTF-8 form. `YamlDocument.Parse(Stream)` refuses a seekable stream that is longer than the limit before reading it, and stops reading any other stream as soon as it passes the limit. `YamlSerializer.Deserialize` and `DeserializeAll` over a `TextReader` count the text in UTF-8 bytes while they read it and stop with the `Limit` error after at most 4,096 characters past the limit, so a very large or endless reader does not fill memory first. A `StringReader` is taken whole, since its text is already in memory, and counted as a string is. The compatibility adapters' `TextReader` overloads, and the SharpYaml adapter's `Stream` overloads, also stop reading once the text is over the limit. The SharpYaml adapter reads only the first document, as SharpYaml does, but holds the whole text, every document included, to `MaxInputBytes`. With no `MaxInputBytes` set, a stream or a reader other than a `StringReader` is still read in pieces, to its end. A stream or reader too large for one buffer, that is more than the runtime can keep in one array or string, ends in the `Limit` error as well (in the adapters, the exception type each one uses for a limit), not in an out-of-memory or I/O error. ## The compatibility adapters The adapters read with `YamlLimits.Default` unless you give them limits. Neither YamlDotNet nor SharpYaml has these settings; the adapters add them so that outside input can be read through them too: | Adapter | Where the limits go | |---|---| | `OneYaml.Compat.YamlDotNet` | `DeserializerBuilder.WithLimits(YamlLimits)`, `YamlStream.Load(TextReader, YamlLimits)`, `new Parser(TextReader, YamlLimits)` | | `OneYaml.Compat.SharpYaml` | `YamlSerializerOptions.Limits` (`null` means `YamlLimits.Default`) | Pass `YamlLimits.Untrusted` for input you do not control. The adapters' own depth settings, `WithMaximumRecursion` and `MaxDepth`, can lower the depth further, never raise it above these limits. In a file that imports an adapter namespace, import the type with `using YamlLimits = OneYaml.YamlLimits;` rather than `using OneYaml;`: the `OneYaml` namespace has types with the same names as the adapters' (`YamlSerializer`, `YamlSerializerOptions`, `YamlException`, `YamlDocument`, `YamlNode`), and importing both makes those names ambiguous (error CS0104). The migration guides show each one ([from YamlDotNet](https://recursiveai.org/oneyaml/csharp/docs/migrate-yamldotnet/#input-you-do-not-control), [from SharpYaml](https://recursiveai.org/oneyaml/csharp/docs/migrate-sharpyaml/#input-you-do-not-control)). ## What the limits do not cover - **Your converters.** Code in a `YamlConverter` runs outside these bounds. A converter that recurses for each level of nesting should call `RuntimeHelpers.EnsureSufficientExecutionStack`, especially under `YamlLimits.None`. - **The compatibility adapters without limits given.** They read with `YamlLimits.Default`, which does not cap input size, scalar size or documents ([The compatibility adapters](#the-compatibility-adapters)). - **Memory without `Untrusted`.** Under `Default` there is no cap on input size, scalar size or documents per stream, so memory grows with the input. A `YamlDocument` keeps the whole text and an index of it. - **What you write.** `Serialize` writes a value that is shared in several places once for each place, so its output can be far larger than the object graph in memory. See [Security](https://github.com/recursive-ai-oss/oneyaml-csharp/blob/7818048c4be434333767c47d66176fc626d1da8e/SECURITY.md) for what the library promises and how to report a problem. # Reader and emitter `Utf8YamlReader` and `Utf8YamlEmitter` are the token-level types under the serializer, in the shape of `Utf8JsonReader` and `Utf8JsonWriter`. You meet the reader inside a custom `YamlConverter`, and you can use the emitter on its own to write YAML token by token. The samples on this page are complete programs that assume implicit usings (the default for new projects); the first shows the `using OneYaml;` line the others leave out. ## Reading tokens in a converter `YamlConverter.Read` gets the reader on the first token of the value and must leave it on the last token of that value: the scalar itself, or the matching `MappingEnd` or `SequenceEnd`. The reader is never on a document marker or an alias: aliases are followed for you, and every expansion counts against the [limits](https://recursiveai.org/oneyaml/csharp/docs/limits/). This converter reads an image either as `name:tag` or as a mapping, and always writes the short form: ```csharp using OneYaml; var pod = YamlSerializer.Deserialize(""" images: - nginx:1.27 - name: envoy tag: "1.30" note: ignored by the converter """)!; Console.WriteLine(string.Join(", ", pod.Images)); // Image { Name = nginx, Tag = 1.27 }, Image { Name = envoy, Tag = 1.30 } Console.Write(YamlSerializer.Serialize(pod)); // images: // - nginx:1.27 // - envoy:1.30 record Pod(List Images); [YamlConverter(typeof(ImageConverter))] record Image(string Name, string Tag); // Reads "name:tag" or a mapping with 'name' and 'tag'; always writes the short form. sealed class ImageConverter : YamlConverter { public override Image Read(ref Utf8YamlReader reader, Type typeToConvert, YamlSerializerOptions options) { if (reader.TokenType == YamlTokenKind.Scalar) { string text = reader.GetString(); int colon = text.LastIndexOf(':'); return colon > 0 ? new Image(text[..colon], text[(colon + 1)..]) : new Image(text, "latest"); } if (reader.TokenType != YamlTokenKind.MappingStart) { throw reader.CreateException("An image is a string or a mapping."); } string name = "", tag = "latest"; while (reader.Read() && reader.TokenType != YamlTokenKind.MappingEnd) { string key = reader.GetString(); // on the key reader.Read(); // on the value switch (key) { case "name": name = reader.GetString(); break; case "tag": tag = reader.GetString(); break; default: reader.Skip(); break; // skips a whole collection too } } return new Image(name, tag); // the reader is left on MappingEnd, the last token of the value } public override void Write(Utf8YamlEmitter writer, Image value, YamlSerializerOptions options) => writer.WriteStringValue($"{value.Name}:{value.Tag}"); } ``` What the reader offers: - `TokenType`: `Scalar`, `MappingStart`, `MappingEnd`, `SequenceStart` or `SequenceEnd`. `Read()` moves to the next token; `Skip()` skips the whole value the reader is on. - `GetString()` returns the text of any scalar. `GetBoolean()`, `GetInt32()`, `GetInt64()`, `GetDouble()`, `GetDecimal()` and `GetBigInteger()` read it under the Core schema and throw a `YamlException` when it is not of that type; `IsNull` tells whether it is a null. - `Tag` and `TryGetTag` give the tag resolved against the document's `%TAG` directives (`!!str` is `tag:yaml.org,2002:str`); `TryGetRawTag` gives it as written, without allocating. `Anchor` and `TryGetAnchor` give the anchor name. - `CreateException(message)` builds a binding error that carries the line, column and path of the current token. The reader is a `ref struct` with no public constructor: it exists only while the serializer calls your converter. To read a whole file by its structure, use [`YamlDocument`](https://recursiveai.org/oneyaml/csharp/docs/editing/) or [untyped values](https://recursiveai.org/oneyaml/csharp/docs/serializer/#untyped-values). ## Writing YAML token by token `Utf8YamlEmitter` writes UTF-8 YAML to an `IBufferWriter`, with the same layout and quoting as `YamlSerializer`. Pass `YamlSerializerOptions` to change the indentation. ```csharp var buffer = new ArrayBufferWriter(); var emitter = new Utf8YamlEmitter(buffer); emitter.WriteStartMapping(); emitter.WritePropertyName("name"); emitter.WriteStringValue("web"); emitter.WritePropertyName("ports"); emitter.WriteStartSequence(); emitter.WriteNumberValue(80); emitter.WriteNumberValue(443); emitter.WriteEndSequence(); emitter.WritePropertyName("base"); emitter.WriteTag("!ref"); emitter.WriteStringValue("defaults"); emitter.WriteEndMapping(); Console.Write(Encoding.UTF8.GetString(buffer.WrittenSpan)); // name: web // ports: // - 80 // - 443 // base: !ref defaults ``` (`ArrayBufferWriter` needs `using System.Buffers;` and `Encoding` needs `using System.Text;`.) The emitter has a `Write…Value` method for strings, booleans, null, every number type and binary data (written as `!!binary`), `WriteTag`, `WriteAnchor` and `WriteAlias` for properties of the next node, and `WritePropertyName` for keys. A string that would read back as something else is quoted, so the output reads back as what you wrote. Calling the emitter in the wrong order (a value where a key is required, a second tag on one node, ending a collection that is not open) throws `InvalidOperationException`, as `Utf8JsonWriter` does. `WriteTag` takes the resolved tag: pass `tag:yaml.org,2002:str` for the core string tag, because `!!str` names a local tag. # Migrating from YamlDotNet There are two ways to move code written against YamlDotNet: 1. **Swap to the adapter.** `OneYaml.Compat.YamlDotNet` provides the YamlDotNet serializer builders and representation model under new namespaces. For the API listed below, changing the package and the `using` lines keeps your call sites, and YAML 1.1 forms read as YamlDotNet reads them. 2. **Move to the `OneYaml` API.** `YamlSerializer` and `YamlDocument` are YAML 1.2, bounded for untrusted input, and can edit a file without losing its comments. This is the route for new code. You can do the first now and the second file by file: the two packages work side by side. ## Before you swap A build that passes after the swap is not yet a finished migration. Check these four things, in this order: | Check | What to look for | |---|---| | **Compile gaps** | API the adapter does not provide fails to build: the scanner, `MergingParser`, type inspectors, node deserializers, event emitters, object-graph visitors, static contexts and comment events. The [full list](https://recursiveai.org/oneyaml/csharp/docs/compat-yamldotnet/#surface-not-provided-gap). | | **Runtime rejections** | `Deserialize(IParser)` with a parser that is not the adapter's own `Parser` throws `NotSupportedException`. A serializer built with `EnsureRoundtrip()` throws `NotSupportedException` when `Serialize` is called: the adapter cannot do what it asks, so it says so instead of ignoring it. | | **Semantic changes** | Serializer output can differ (more quoting, escapes), more valid YAML is read, the class of an exception can differ, and after you change a node `YamlStream.Save` writes without comments. See [Check after the swap](#check-after-the-swap) and the [list of differences](https://recursiveai.org/oneyaml/csharp/docs/compat-yamldotnet/). | | **Safety limits** | The depth limit and the alias budget always apply, and `WithMaximumRecursion` cannot raise the depth above the library limit. For input you do not control, pass `YamlLimits.Untrusted` to `WithLimits` ([Input you do not control](#input-you-do-not-control)). `WithLimits` is an addition: YamlDotNet has no such setting. | ## Swap to the adapter ```sh dotnet remove package YamlDotNet dotnet add package OneYaml.Compat.YamlDotNet ``` | Before | After | |---|---| | `using YamlDotNet.Serialization;` | `using OneYaml.Compat.YamlDotNet.Serialization;` | | `using YamlDotNet.Serialization.NamingConventions;` | `using OneYaml.Compat.YamlDotNet.Serialization.NamingConventions;` | | `using YamlDotNet.RepresentationModel;` | `using OneYaml.Compat.YamlDotNet.RepresentationModel;` | | `using YamlDotNet.Core;` | `using OneYaml.Compat.YamlDotNet.Core;` | Before, with YamlDotNet 18.1.0: ```csharp using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; IDeserializer deserializer = new DeserializerBuilder() .WithNamingConvention(CamelCaseNamingConvention.Instance) .IgnoreUnmatchedProperties() .Build(); var service = deserializer.Deserialize("name: web\nreplicas: 2\nlegacy: true\n"); ISerializer serializer = new SerializerBuilder() .WithNamingConvention(CamelCaseNamingConvention.Instance) .Build(); Console.Write(serializer.Serialize(service)); // name: web // replicas: 2 class Service { public string Name { get; set; } = ""; public int Replicas { get; set; } } ``` After, with the adapter; only the `using` lines changed, and the output is the same: ```csharp using OneYaml.Compat.YamlDotNet.Serialization; using OneYaml.Compat.YamlDotNet.Serialization.NamingConventions; IDeserializer deserializer = new DeserializerBuilder() .WithNamingConvention(CamelCaseNamingConvention.Instance) .IgnoreUnmatchedProperties() .Build(); var service = deserializer.Deserialize("name: web\nreplicas: 2\nlegacy: true\n"); ISerializer serializer = new SerializerBuilder() .WithNamingConvention(CamelCaseNamingConvention.Instance) .Build(); Console.Write(serializer.Serialize(service)); // name: web // replicas: 2 class Service { public string Name { get; set; } = ""; public int Replicas { get; set; } } ``` ### What the adapter covers - **`Serialization`:** `DeserializerBuilder` and `SerializerBuilder` with `WithNamingConvention`, `WithEnumNamingConvention`, `WithTypeConverter`, `WithTagMapping`, `WithAttributeOverride`, `IgnoreUnmatchedProperties`, `WithCaseInsensitivePropertyMatching`, `WithDuplicateKeyChecking`, `WithAttemptingUnquotedStringTypeDeserialization`, `WithMaximumRecursion`, `WithTypeMapping`, `WithObjectFactory`, `ConfigureDefaultValuesHandling`, `WithQuotingNecessaryStrings`, `WithDefaultScalarStyle`, `DisableAliases`, `JsonCompatible`, `WithNewLine` and `WithIndentedSequences`; `IDeserializer`, `ISerializer`, the six naming conventions, `[YamlMember]`, `[YamlIgnore]`, `[YamlConverter]`, `IYamlTypeConverter` and `IYamlConvertible`. - **`RepresentationModel`:** `YamlStream`, `YamlDocument`, `YamlMappingNode`, `YamlSequenceNode`, `YamlScalarNode`, visitors, node positions. - **`Core`:** `Parser`, `Emitter`, the event classes, `Mark` and the exception types. The [list of differences](https://recursiveai.org/oneyaml/csharp/docs/compat-yamldotnet/#surface-not-provided-gap) names what is not provided, such as the scanner, `MergingParser`, type inspectors, node deserializers and static contexts. Code that uses them does not compile, so the gap shows at build time. The adapter's representation model reads a file and writes it back as it was when nothing changed: ```csharp using OneYaml.Compat.YamlDotNet.RepresentationModel; var stream = new YamlStream(); stream.Load(new StringReader("# service settings\nname: web\nreplicas: 2 # set by CI\n")); var root = (YamlMappingNode)stream.Documents[0].RootNode; Console.WriteLine(root["replicas"]); // 2 var text = new StringWriter(); stream.Save(text); // nothing was changed, so the text comes back as it was read Console.Write(text); // # service settings // name: web // replicas: 2 # set by CI ``` After you change a node, `Save` writes the model again without comments, as YamlDotNet does. To change a file and keep its comments, use `YamlDocument` ([below](#editing-a-file)). ### Check after the swap The adapter reproduces YamlDotNet's documented behaviour, not its defects. Read [the full list of differences](https://recursiveai.org/oneyaml/csharp/docs/compat-yamldotnet/), each with a repro, and check these first: - **Output that reads back.** Strings that a reader would take for something else are quoted (`'true'`, `'yes'`, `'123'`), characters that YamlDotNet writes in a way that changes on reading are escaped, and long keys are written so that they read back. Snapshot tests of serializer output may need updating. - **Comments survive an unchanged round trip.** `YamlStream.Save` returns the loaded text when nothing changed; YamlDotNet writes it again without comments and with `...` after each document. - **More valid YAML is read.** Some inputs that YamlDotNet refuses or throws `ArgumentException` on are valid YAML 1.2 and are read. - **Limits stay on.** The deserializer enforces the alias budget and the depth limit. An alias into `object` or a collection gives back the same instance, and every use is charged to the alias budget as if it were expanded, so a billion-laughs input fails with a `YamlException`. For input you do not control, call `WithLimits(YamlLimits.Untrusted)` on the builder, or pass `YamlLimits.Untrusted` to `YamlStream.Load` or `new Parser(reader, limits)` ([Input you do not control](#input-you-do-not-control)). - **Exception classes.** `SyntaxErrorException`, `SemanticErrorException`, `MaximumRecursionLevelReachedException` and `AnchorNotFoundException` are raised, but which class a given error gets can differ from YamlDotNet. Catch `YamlException` if you can. - **Line breaks.** The serializer writes the platform line break, as YamlDotNet does. ### Input you do not control The adapter adds three members that YamlDotNet does not have, so that reading can keep to the [limits](https://recursiveai.org/oneyaml/csharp/docs/limits/) of the `OneYaml` library: `DeserializerBuilder.WithLimits(YamlLimits)`, `YamlStream.Load(TextReader, YamlLimits)` and `new Parser(TextReader, YamlLimits)`. Without them the adapter reads with `YamlLimits.Default`. For uploads, request bodies and files from other systems, pass `YamlLimits.Untrusted`, which also bounds the input size, the size of a scalar and the number of documents. `YamlLimits` is in the `OneYaml` namespace; import it with `using YamlLimits = OneYaml.YamlLimits;`, not `using OneYaml;`, because that namespace has its own `YamlException`, `YamlDocument`, `YamlNode` and `YamlIgnoreAttribute`, and importing it beside the adapter's namespaces makes those names ambiguous (error CS0104). ```csharp using OneYaml.Compat.YamlDotNet.Core; using OneYaml.Compat.YamlDotNet.Serialization; using YamlLimits = OneYaml.YamlLimits; string upload = new string('[', 129) + new string(']', 129); // nested 129 deep // WithLimits is not in YamlDotNet. It takes the limits of the OneYaml library; Untrusted is its preset for outside input. IDeserializer deserializer = new DeserializerBuilder() .WithLimits(YamlLimits.Untrusted) .Build(); try { deserializer.Deserialize(upload); } catch (YamlException e) { Console.WriteLine(e.Message); // The nesting depth is larger than the limit MaxDepth (128). (line 1, column 129) } // The default limits allow the same text. Console.WriteLine(new DeserializerBuilder().Build().Deserialize(upload) is not null); // True ``` Code that uses these members no longer compiles against YamlDotNet itself. ## Move to the OneYaml API ```sh dotnet add package OneYaml ``` If the project still references `OneYaml.Compat.YamlDotNet`, keep `OneYaml` at the same version: the adapter depends on that exact version. ```csharp using OneYaml; // camelCase keys and skipping unknown keys are the defaults; no builder is needed. var service = YamlSerializer.Deserialize("name: web\nreplicas: 2\nlegacy: true\n")!; Console.Write(YamlSerializer.Serialize(service)); // name: web // replicas: 2 class Service { public string Name { get; set; } = ""; public int Replicas { get; set; } } ``` | YamlDotNet | OneYaml | |---|---| | `new DeserializerBuilder().Build().Deserialize(yaml)` | `YamlSerializer.Deserialize(yaml)` | | `new SerializerBuilder().Build().Serialize(value)` | `YamlSerializer.Serialize(value)` | | `WithNamingConvention(CamelCaseNamingConvention.Instance)` | the default (`PropertyNamingPolicy = JsonNamingPolicy.CamelCase`) | | `UnderscoredNamingConvention`, `HyphenatedNamingConvention` | `JsonNamingPolicy.SnakeCaseLower`, `JsonNamingPolicy.KebabCaseLower` | | `NullNamingConvention` (YamlDotNet's default) | `PropertyNamingPolicy = null` | | `IgnoreUnmatchedProperties()` | the default; `UnmappedMemberHandling = Disallow` is YamlDotNet's default behaviour | | `WithCaseInsensitivePropertyMatching()` | `PropertyNameCaseInsensitive = true` | | `WithDuplicateKeyChecking()` | always on: a repeated key is an error | | `WithMaximumRecursion(n)` | `Limits = YamlLimits.Default with { MaxDepth = n }` | | `ConfigureDefaultValuesHandling(OmitNull)` | `DefaultIgnoreCondition = YamlIgnoreCondition.WhenWritingNull` | | `WithTypeConverter(IYamlTypeConverter)` | `Converters = { new MyConverter() }`, a `YamlConverter` ([Converters](https://recursiveai.org/oneyaml/csharp/docs/serializer/#converters)) | | `WithTagMapping("!x", typeof(X))` | `[YamlPolymorphic(DiscriminatorKind = YamlDiscriminatorKind.Tag)]` and `[YamlDerivedType(typeof(X), "x")]` on a base type ([Polymorphism](https://recursiveai.org/oneyaml/csharp/docs/serializer/#polymorphism)) | | `[YamlMember(Alias = "x")]`, `[YamlMember(Order = n)]` | `[YamlPropertyName("x")]`, `[YamlPropertyOrder(n)]` | | `[YamlIgnore]` | `[YamlIgnore]` (in the `OneYaml` namespace) | | `MergingParser` | `MergeKeys = true` | | `YamlStream`, `YamlMappingNode`, `YamlScalarNode` | `YamlDocument`, `YamlNode` ([Editing](https://recursiveai.org/oneyaml/csharp/docs/editing/)) | | `YamlException.Start` | `YamlException.Line`, `Column`, `Offset`, plus `Category` and `Path` | | static builders with a separate generator package | `[YamlSerializable]` on a `YamlSerializerContext`; the generator is in the package ([Source generation](https://recursiveai.org/oneyaml/csharp/docs/serializer/#source-generation-and-native-aot)) | Behaviour to expect when you move: - **YAML 1.2, not 1.1.** `yes`, `on` and `y` are strings, so reading `enabled: yes` into a `bool` is an error; write `true`. `017` is 17, not octal 15; `0o17` is 15. - **Untyped values have types.** `Deserialize` gives `long`, `double`, `bool` and `null` for plain scalars, where YamlDotNet gives strings ([Untyped values](https://recursiveai.org/oneyaml/csharp/docs/serializer/#untyped-values)). - **Repeated keys are an error**, where YamlDotNet keeps the last one by default. - **`null` members are written** as `null` unless you set `DefaultIgnoreCondition`. ## Editing a file YamlDotNet's representation model writes a file again from the model, so comments and blank lines are lost when you save a change. `YamlDocument` changes only the value you set: ```csharp // Instead of YamlStream: load, change one value, save, and keep the comments. using var doc = YamlDocument.Parse("# service settings\nname: web\nreplicas: 2 # set by CI\n"); doc.Root["replicas"] = 3; Console.Write(doc); // # service settings // name: web // replicas: 3 # set by CI ``` See [Editing](https://recursiveai.org/oneyaml/csharp/docs/editing/) for pointers, styles and aliases, and [Patches](https://recursiveai.org/oneyaml/csharp/docs/patches/) to apply a JSON Patch. # Migrating from SharpYaml SharpYaml 3 and OneYaml share the System.Text.Json shape, so most code moves with few changes. There are two ways: 1. **Swap to the adapter.** `OneYaml.Compat.SharpYaml` provides SharpYaml's `YamlSerializer`, `YamlSerializerOptions`, attributes and exceptions under a new namespace. For the API listed below, changing the package and the `using` lines keeps your call sites. 2. **Move to the `OneYaml` API.** It adds what the adapter leaves out (converters, polymorphism, extension data, generated contexts) and a `YamlDocument` that edits a file without losing its comments. This is the route for new code. ## Before you swap A build that passes after the swap is not yet a finished migration. Check these four things, in this order: | Check | What to look for | |---|---| | **Compile gaps** | Custom converters (`YamlConverter`, `YamlReader`, `YamlWriter`), the `IYamlOn*` hooks, polymorphism, extension data, generated contexts (`Ctx.Default` does not exist), and the `Model` and `Syntax` namespaces are not provided, so code that uses them fails to build. The [full list](https://recursiveai.org/oneyaml/csharp/docs/compat-sharpyaml/#what-is-left-out). | | **Runtime rejections** | Some options compile but throw `NotSupportedException` at first use: a schema other than Core, `DuplicateKeyHandling` other than the default, `DictionaryKeyPolicy`, `Populate`, some `DefaultIgnoreCondition` values, `UnsafeAllowDeserializeFromTagTypeName` and a custom `TypeInfoResolver`. The layout options the writer cannot follow are refused too: `BlockSequenceMappingStyle` other than `Default` or `Compact`, `BlockSequenceSequenceStyle` other than `Default` or `Expanded`, `ScalarStylePreferences` with `PreferPlainStyle` or `PreferQuotedForAmbiguousScalars` set, and `WriteIndented = false`. `ReferenceHandling` other than `None` is refused when writing; reading follows aliases with any value. | | **Semantic changes** | Output can differ (whole floats, quoting of YAML 1.1 look-alikes), overflowing numbers are an error, and encodings without a byte order mark are detected. See [Check after the swap](#check-after-the-swap) and the [list of differences](https://recursiveai.org/oneyaml/csharp/docs/compat-sharpyaml/). | | **Safety limits** | `MaxDepth` defaults to 64, as in SharpYaml, and is never raised above the library limit. For input you do not control, set `Limits = YamlLimits.Untrusted` ([Input you do not control](#input-you-do-not-control)). `Limits` is an addition: SharpYaml has no such option. | ## Swap to the adapter ```sh dotnet remove package SharpYaml dotnet add package OneYaml.Compat.SharpYaml ``` | Before | After | |---|---| | `using SharpYaml;` | `using OneYaml.Compat.SharpYaml;` | | `using SharpYaml.Serialization;` | `using OneYaml.Compat.SharpYaml.Serialization;` | Before, with SharpYaml 3.14.0: ```csharp using System.Text.Json; using SharpYaml; var options = new YamlSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; var service = YamlSerializer.Deserialize("name: web\nreplicas: 2\n", options)!; Console.Write(YamlSerializer.Serialize(service with { Replicas = 3 }, options)); // name: web // replicas: 3 record Service(string Name, int Replicas); ``` After, with the adapter; only the `using` line changed, and the output is the same: ```csharp using System.Text.Json; using OneYaml.Compat.SharpYaml; var options = new YamlSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; var service = YamlSerializer.Deserialize("name: web\nreplicas: 2\n", options)!; Console.Write(YamlSerializer.Serialize(service with { Replicas = 3 }, options)); // name: web // replicas: 3 record Service(string Name, int Replicas); ``` ### What the adapter covers - `YamlSerializer.Serialize`, `Deserialize` and `TryDeserialize` over strings, spans, readers, writers and streams, with `Type` and generic forms. - `YamlSerializerOptions` and its enums, `ReflectionYamlTypeInfoResolver`, `YamlTypeInfo`. - The attributes `YamlPropertyName`, `YamlIgnore`, `YamlInclude`, `YamlRequired`, `YamlPropertyOrder`, `YamlConstructor` and `YamlBlockSequenceItemStyle`, with the System.Text.Json attributes as the fallback. - `YamlException`, `SyntaxErrorException`, `SemanticErrorException` and `Mark`. What it does not provide, and the options it rejects, are in [Before you swap](#before-you-swap). ### Check after the swap Read [the full list of differences](https://recursiveai.org/oneyaml/csharp/docs/compat-sharpyaml/), each with a repro. The ones most likely to matter: - **Output.** Whole floats are written with a fraction (`100.0`) and YAML 1.1 look-alikes are quoted (`'y'`, `'on'`). - **Numbers that overflow** their type are an error, where SharpYaml turns them into infinity. - **Streams in UTF-16 or UTF-32** without a byte order mark are detected, as YAML 1.2 says. - **Exceptions.** Syntax errors are `SyntaxErrorException`; every other error is a `YamlException`. ### Input you do not control `YamlSerializerOptions.Limits` is not in SharpYaml. It sets the [limits](https://recursiveai.org/oneyaml/csharp/docs/limits/) of the `OneYaml` library for every read through those options, and when it is `null`, the default, reading uses `YamlLimits.Default`. For uploads, request bodies and files from other systems, set it to `YamlLimits.Untrusted`, which also bounds the input size and the size of a scalar, and bounds alias expansion more tightly. `MaxDepth` can lower the depth further, never raise it. `YamlLimits` is in the `OneYaml` namespace; import it with `using YamlLimits = OneYaml.YamlLimits;`, not `using OneYaml;`, because that namespace has its own `YamlSerializer`, `YamlSerializerOptions` and `YamlException`, and importing it beside `OneYaml.Compat.SharpYaml` makes those names ambiguous (error CS0104). ```csharp using OneYaml.Compat.SharpYaml; using YamlLimits = OneYaml.YamlLimits; // 17 short lines whose aliases double at each level: a small text that stands for a very large value. string upload = "a0: &a0 [x, x]\n" + string.Concat(Enumerable.Range(1, 16).Select(i => $"a{i}: &a{i} [*a{i - 1}, *a{i - 1}]\n")); // Limits is not in SharpYaml. It takes the limits of the OneYaml library; Untrusted is its preset for outside input. var options = new YamlSerializerOptions { Limits = YamlLimits.Untrusted }; try { YamlSerializer.Deserialize(upload, options); } catch (YamlException e) { Console.WriteLine(e.Message); // (Lin: 14, Col: 17, Chr: 293) - (Lin: 14, Col: 17, Chr: 293): Expanding the aliases exceeds the budget set by YamlLimits (AliasNodeFactor 2, AliasNodeFloor 100000, AliasByteFactor 2, AliasByteFloor 8388608). } // The default limits allow the same text. Console.WriteLine(YamlSerializer.Deserialize(upload) is not null); // True ``` Code that sets `Limits` no longer compiles against SharpYaml itself. ## Move to the OneYaml API ```sh dotnet add package OneYaml ``` If the project still references `OneYaml.Compat.SharpYaml`, keep `OneYaml` at the same version: the adapter depends on that exact version. ```csharp using OneYaml; // camelCase is the default naming policy, so the options can go. var service = YamlSerializer.Deserialize("name: web\nreplicas: 2\n")!; Console.Write(YamlSerializer.Serialize(service with { Replicas = 3 })); // name: web // replicas: 3 record Service(string Name, int Replicas); ``` | SharpYaml | OneYaml | |---|---| | `SharpYaml.YamlSerializer` | `OneYaml.YamlSerializer` | | `PropertyNamingPolicy`, `PropertyNameCaseInsensitive`, `DefaultIgnoreCondition`, `IndentSize` | the same names on `YamlSerializerOptions` | | `UnmappedMemberHandling` (`JsonUnmappedMemberHandling`) | `UnmappedMemberHandling` (`YamlUnmappedMemberHandling`) | | `MaxDepth` | `Limits = YamlLimits.Default with { MaxDepth = n }` ([Limits](https://recursiveai.org/oneyaml/csharp/docs/limits/)) | | `DuplicateKeyHandling` | a repeated key is always an error; `YamlDocumentOptions.AllowDuplicateKeys` lets you edit such a file | | `Schema`, `UseSchema` | the Core schema; `MergeKeys = true` for `<<` | | `ReferenceHandling = Preserve` to read aliases | aliases are read by default, as shared instances, within the alias budget | | `WriteIndented = false` | no equivalent: output is block style; the compat package throws when writing with it | | `[YamlSerializable]` on a `YamlSerializerContext` | the same shape, `OneYaml.YamlSerializerContext` ([Source generation](https://recursiveai.org/oneyaml/csharp/docs/serializer/#source-generation-and-native-aot)) | | `YamlConverter` with `YamlReader` and `YamlWriter` | `YamlConverter` with `Utf8YamlReader` and `Utf8YamlEmitter` ([Converters](https://recursiveai.org/oneyaml/csharp/docs/serializer/#converters)) | | `[YamlPolymorphic]`, `[YamlDerivedType]` | the same names, with a key or a tag discriminator ([Polymorphism](https://recursiveai.org/oneyaml/csharp/docs/serializer/#polymorphism)) | | `YamlSyntaxTree.WithTextChange(start, length, text)` | `YamlDocument` indexers and `Set`, which write the YAML text for you ([Editing](https://recursiveai.org/oneyaml/csharp/docs/editing/)) | | `Model.YamlStream` | `YamlDocument` | Behaviour to expect when you move: - **The default naming policy differs.** SharpYaml uses member names as declared unless you set a policy; OneYaml uses camel case. If your SharpYaml code sets no `PropertyNamingPolicy`, set `PropertyNamingPolicy = null` to keep the same keys. - **Aliases are read.** SharpYaml refuses aliases into `object` and dictionaries unless `ReferenceHandling` is `Preserve`; OneYaml reads them as shared instances and bounds their expansion ([Limits](https://recursiveai.org/oneyaml/csharp/docs/limits/)). - **Repeated keys are always an error** in the serializer, and only the Core schema is available. ## Editing a file SharpYaml's syntax tree keeps every byte outside a range you replace, but you write the YAML text for the new value yourself, and find the range. `YamlDocument` takes a .NET value and writes it in the style of the old one: ```csharp using var doc = YamlDocument.Parse("name: web # the public site\nreplicas: 2\n"); doc.Root["replicas"] = 3; Console.Write(doc); // name: web # the public site // replicas: 3 ``` See [Editing](https://recursiveai.org/oneyaml/csharp/docs/editing/) and [Patches](https://recursiveai.org/oneyaml/csharp/docs/patches/). # OneYaml.Compat.YamlDotNet: differences from YamlDotNet 18.1.0 The package covers a subset of YamlDotNet's API: the common paths through the serializer builders (`SerializerBuilder`, `DeserializerBuilder`, naming conventions, `IYamlTypeConverter`, the attributes) and the representation model (`YamlStream` and its nodes), with the parser events they use. It reproduces the documented behaviour of YamlDotNet for those paths, never its defects. It is tested against YamlDotNet 18.1.0: every difference below is checked against that version by a differential test in `tests/OneYaml.Compat.Tests`. Newer YamlDotNet releases are not tested. Not covered, so code that uses it does not build: - static contexts (`StaticSerializerBuilder`) and other generated serializer code; - type inspectors, node deserializers, object factories, event emitters and object-graph visitors; - the scanner, `MergingParser` and comment events; - the rest of the [surface not provided](#surface-not-provided-gap). The adapter's types are its own, in the `OneYaml.Compat.YamlDotNet` namespaces; they are not YamlDotNet's types. Code you control switches with a package swap and a `using` change: `using YamlDotNet.Serialization;` becomes `using OneYaml.Compat.YamlDotNet.Serialization;` (and likewise for `RepresentationModel` and `Core`). A third-party library that takes or returns YamlDotNet types (a converter, a node, a builder) still needs YamlDotNet itself and cannot be handed the adapter's types. For the steps of a migration, what to check after the swap, and the move to the `OneYaml` API, see [Migrating from YamlDotNet](https://recursiveai.org/oneyaml/csharp/docs/migrate-yamldotnet/). The adapter reads with the library's default limits unless you pass limits to `DeserializerBuilder.WithLimits`, `YamlStream.Load` or `new Parser`; those members are additions, not in YamlDotNet, and `YamlLimits.Untrusted` is the setting for input you do not control ([Limits](https://recursiveai.org/oneyaml/csharp/docs/limits/#the-compatibility-adapters)). Import the type with `using YamlLimits = OneYaml.YamlLimits;`, not `using OneYaml;`: that namespace has its own `YamlException`, `YamlDocument`, `YamlNode` and `YamlIgnoreAttribute`, so importing it beside the adapter's namespaces makes those names ambiguous (error CS0104). Each class has a minimal repro. Label: **intended (spec)** = the YAML 1.2 text or a safer behaviour is kept on purpose; **intended (bug not reproduced)** = YamlDotNet misbehaves and the package does not copy it; **gap** = documented API or behaviour the package does not provide. ## Output (Serialize / YamlStream.Save) | # | Difference | Repro | Label | |---|---|---|---| | O1 | Strings YAML 1.1 reads as non-strings are quoted | `Serialize("true")` → YamlDotNet `true`, package `'true'`; also `y`, `Y`, `yes`, `123`, `~` (the empty string is not a difference: both write `''`) | intended (spec: a reader of the output gets the string back) | | O2 | Non-BMP characters written raw | `Serialize("\U0001F600")` → YamlDotNet `"\U0001F600"`, package the character | intended (spec) | | O3 | Tab inside a double-quoted scalar written as `\t` | `Serialize("a\tb")` in double-quoted style | intended (spec; YamlDotNet writes a raw tab) | | O3a | A plain scalar with a tab is quoted | `Serialize("a\tb")` | intended (spec; a tab in a plain scalar does not read back the same in every reader) | | O3b | U+0085 and U+2028/U+2029 are written escaped | `Serialize("a\u2028b")` → YamlDotNet folds the character into a line break and the string changes on reading; the package writes `\L` style escapes | intended (bug not reproduced: data loss) | | O3c | U+FEFF is written escaped | `Serialize("\uFEFFa")` → YamlDotNet writes it raw, a reader takes it for a byte order mark | intended (bug not reproduced: data loss) | | O4 | `OmitEmptyCollections` keeps empty strings | `class C { string S = ""; }` with `ConfigureDefaultValuesHandling(OmitEmptyCollections)`: YamlDotNet drops `S` (a string is an `IEnumerable`) | intended (bug not reproduced) | | O5 | `DateTimeOffset` is one ISO string | `Serialize(DateTimeOffset.UnixEpoch)` → YamlDotNet writes every property of the struct | intended (bug not reproduced) | | O6 | `YamlStream.Save` keeps comments and layout of text that was loaded and not changed | load `# c\na: 1`, save: package returns the input; YamlDotNet re-emits `a: 1` | intended (kept comments) | | O7 | After an edit `Save` re-emits from the model without comments; YamlDotNet adds `...` after each document, the package does not | `Save` of a built model: YamlDotNet ends with `...`, package does not | gap (no edit-sync into the lossless document; document end marker not written) | | O8 | Anchors from `Save(w, assignAnchors: true)` are `o0`, `o1`..; YamlDotNet writes numeric names such as `&466756907` | any shared node | intended (bug not reproduced) | | O9 | `Int128`, `UInt128` and `Half` are written as numbers | `Serialize((Int128)5)` → YamlDotNet writes `{}` (the value is lost), package `5`; the same for `UInt128` and `Half`. Reading them back: decimal numbers read the same in both; the package also reads `0x10` into `Int128` and `UInt128`, and `.inf`, `-.inf` and `.nan` into `Half` (the spellings it writes), where YamlDotNet throws; a number too large for a `Half` is an error here, where YamlDotNet gives infinity | intended (bug not reproduced: data loss) | ## Input (Deserialize / YamlStream.Load) | # | Difference | Repro | Label | |---|---|---|---| | I1 | Spec-valid YAML YamlDotNet rejects is read | `{foo\n: bar}` (4MUZ/02); `foo: !!seq\n - !!str a` with tagged collections (57H4, 6JWB, EHF6); `foo: 1\n\nbar: 2` (DK95/04); `k: {\n k\n :\n v\n }` (VJP3/01); `{ foo : !!str, !!str : bar }` (WZ62) | intended (spec) | | I2 | YamlDotNet throws `ArgumentException` on some inputs, the package reads them | `- ? : x` and `- :` followed by an indented line (M2N8/00, UKK6/00) | intended (bug not reproduced) | | I3 | Block scalar with kept trailing lines | the cases need their exact spaces: JEF9/01 is `- |+` followed by a line of three spaces (`- |+\n \n`), spec `"\n"`; also JEF9/02 and L24T/01; YamlDotNet drops or adds a break | intended (spec) | | I4 | Empty scalar tagged `!!str` | `- !!str` → spec `""`, YamlDotNet `null` (LE5A) | intended (spec) | | I5 | Empty key | `{ ? foo :, : bar }` (FRK4): YamlDotNet reads key `""`, but refuses an empty key in every other shape (`: a`, `[ : x ]`); the package refuses all with "A mapping key cannot be null." | gap (YamlDotNet is inconsistent; the common case is matched) | | I6 | `char` from a longer string | `Deserialize("abc")` → YamlDotNet `'a'`, package throws | intended (bug not reproduced) | | I7 | Unquoted decimals typed as `double` | `WithAttemptingUnquotedStringTypeDeserialization`: `123456789.123` → YamlDotNet `float` 123456790, package `double` | intended (bug not reproduced; integers match) | | I8 | Empty member name | naming convention applied to `""` crashes YamlDotNet (`IndexOutOfRange`); package returns `""` | intended (bug not reproduced) | | I9 | More collection targets | `Deserialize>("[a]")`, `IReadOnlyDictionary` → YamlDotNet throws, package reads | intended (more than the target) | | I10 | Exception types | `SemanticErrorException` vs `SyntaxErrorException` cannot be told apart by the engine; syntax/encoding errors map to `SyntaxErrorException`, unresolved tags to `SemanticErrorException`, limits to `MaximumRecursionLevelReachedException`, undefined aliases to `AnchorNotFoundException` (by message text) | gap (the class of an error can differ) | | I11 | Limits stay on | The deserializer enforces the alias budget and the depth limit. An alias into `object` or a collection gives back the same instance, and every use is charged to the alias budget as if it were expanded, so a billion-laughs input fails with a `YamlException` (an `AliasExpansionLimitExceededException`, a subclass of `MaximumRecursionLevelReachedException`, when the alias budget is the limit reached). For input you do not control, call `WithLimits(YamlLimits.Untrusted)` on the builder, or pass `YamlLimits.Untrusted` to `YamlStream.Load` or `new Parser(reader, limits)`. These three members are not in YamlDotNet, and a deserializer reading from such a parser keeps its limits. `WithMaximumRecursion` (default 130 read / 50 write) can lower the depth, never raise it above the library's depth limit. Representation-model walks (`ToString`, `Equals`, visitors) are bounded by the same node and text budgets. | intended (safety; compat never loosens limits) | | I12 | Positions | node `Start`/`End` and exception marks use characters (UTF-16 units), so `Index` and `Column` count a character outside the Basic Multilingual Plane as two, as YamlDotNet does; collection `End` equals its `Start` as in YamlDotNet | matched | | I12a | A leading byte order mark (U+FEFF) is not counted in positions | `Load(new StringReader("\uFEFFa: 1\nb: 2\n"))`: YamlDotNet counts the mark as one character, so from the end of the first node on, node and error `Index` values are one higher than for the same text without the mark. The package gives the positions of the text without the mark. YamlDotNet gives those positions too when the text comes through a `StreamReader`, which drops the mark, so its positions depend on how the text arrives; the package's are the same for a string, a `StringReader` or a `StreamReader` | intended (bug not reproduced: positions that depend on the input type) | | I12b | Where a syntax error is placed | the error mark is normally at YamlDotNet's line and column but not always: `a: b: c` → YamlDotNet line 1, column 5, package column 6; `x: 1\nyy: 'q\n` (unclosed quote) → YamlDotNet at the opening quote (line 2, column 5), package at the end of the input (line 3, column 1). The units agree (see I12); only the point the error is reported at differs. The end of an error mark can also differ in width (`a: 1\nb: "😀" x\n` ends at column 10 in YamlDotNet, 9 in the package) | gap (the engine finds the error at another point) | ## Bugs not reproduced | Area | Hazard | Package | |---|---|---| | Long keys | Keys longer than 1,024 characters are written as implicit keys, which readers reject | A key too long to be implicit is written after `? ` with the colon on its own line, for strings, for scalars of a loaded model, and in flow mappings. A key that is not a string (a long number, such as a `BigInteger` key) is written in its raw form after `? `, so it reads back as a number and not as a quoted string. | | Anchor names | Generated anchor names can collide with names already in the model, so an alias can resolve to the wrong node on reading | Generated names skip every name the model already uses. | | Nesting depth | Nesting bounded only by the recursion setting, so a huge setting can exhaust the stack | The effective depth is the smaller of the setting and the library's depth limit, and the stack is checked as well, on read and on write. | | Model walks | Walks over a model built from aliases (`ToString`, `Equals`, visitors) have no bound; a billion-laughs model takes minutes or memory without limit | Every walk is bounded in depth, nodes and text; a walk past a bound ends in `MaximumRecursionLevelReachedException`. `GetHashCode` is not such a walk: it visits each distinct node once (see Key hashing). | | Key hashing | Complex keys that agree on their first nodes are hard to tell apart by hash, so a mapping with thousands of such keys loads slowly (quadratic) | `GetHashCode` of a node covers the whole structure and depends on the structure alone, so equal nodes always hash alike however they share nodes and however large they are. A node shared by several parents is hashed once, so a mapping whose complex keys agree on a long prefix loads without the quadratic slowdown; while a document loads, its keys share a memo of hashed nodes that belongs to the load and is dropped when it ends. The walk keeps its own stack and takes time in proportion to the nodes in memory, never to the size of the unrolled tree, so it has no node or depth bound and never throws. A model that contains itself has no finite structure; its hash covers the node's kind, tag and size. | ## Surface not provided (gap) `Core` `Scanner`, `MergingParser`, most `Parser` internals; `IObjectFactory`, node deserializers, type inspectors, event emitters, object-graph visitors; `WithTypeDiscriminatingNodeDeserializer`, `WithEnforceNullability`/`WithEnforceRequiredMembers`; static contexts (`StaticSerializerBuilder`); `WithYamlFormatter`; registration-location overloads; `YamlMappingNode.FromObject`; comment events; `WithTypeResolver`. `EmitDefaults` is an error in YamlDotNet and absent here. `EnsureRoundtrip()` builds, but `Serialize` then throws `NotSupportedException`, since the adapter cannot do what it asks. `ParserExtensions` has `Consume`/`Accept`/`TryConsume`-style members used by converters only; `Peek`/`Allow`/`Expect`/`TryFindMappingEntry` are missing. `Deserialize(IParser)` accepts only the package's own `Parser`. ## Reproduced on purpose `yes/on/y/true` (any case) read as booleans for typed targets; ints with `0x`, `0b`, leading-zero octal, `_`, base 60; `0o17` rejected; `.inf`/`.nan`; null → default for value types; `Deserialize("")` → default; last-wins duplicate keys (error with `WithDuplicateKeyChecking`); unknown property → `YamlException`; read-only property counts as unknown; no `<<` merge; two documents with the string overloads → error; the six naming conventions (verified identifier by identifier). # OneYaml.Compat.SharpYaml: differences from SharpYaml 3.14.0 The package covers a subset of SharpYaml's API: the common serializer paths (`YamlSerializer`, `YamlSerializerOptions`, the attributes and the exceptions), listed in [What is covered](#what-is-covered). It reproduces the documented behaviour of SharpYaml for those paths, never its defects. It is tested against SharpYaml 3.14.0: every difference below is checked against that version by a differential test in `tests/OneYaml.Compat.Tests`. Newer SharpYaml releases are not tested. Not covered, so code that uses it does not build: - custom converters, the `IYamlOn*` hooks, polymorphism and extension data; - generated contexts: a `YamlSerializerContext` compiles, but nothing is generated for it; - the `Model` and `Syntax` namespaces, schemas, the scanner, parser and emitter; - the rest of [What is left out](#what-is-left-out), which also lists the options that compile but fail at first use. The adapter's types are its own, in the `OneYaml.Compat.SharpYaml` namespaces; they are not SharpYaml's types. Code you control switches with a package swap and a `using` change: `using SharpYaml;` becomes `using OneYaml.Compat.SharpYaml;` (and `SharpYaml.Serialization` becomes `OneYaml.Compat.SharpYaml.Serialization`). A third-party library that takes or returns SharpYaml types (options, a type info, a context) still needs SharpYaml itself and cannot be handed the adapter's types. For the steps of a migration, what to check after the swap, and the move to the `OneYaml` API, see [Migrating from SharpYaml](https://recursiveai.org/oneyaml/csharp/docs/migrate-sharpyaml/). The adapter reads with the library's default limits unless `YamlSerializerOptions.Limits` is set; that option is an addition, not in SharpYaml, and `YamlLimits.Untrusted` is the setting for input you do not control ([Limits](https://recursiveai.org/oneyaml/csharp/docs/limits/#the-compatibility-adapters)). Classes: **Intended** (a deliberate choice, kept), **Fix** (SharpYaml 3.14.0 writes output that does not read back, or gives a result the YAML does not say; the package does not reproduce it), **Gap** (a behaviour the package does not reproduce yet). ## What is covered - `YamlSerializer.Serialize`, `Deserialize` and `TryDeserialize` over `string`, `ReadOnlySpan`, `TextReader`, `Stream`, `TextWriter` and `IBufferWriter`, each with `Type`/generic, options, context and `YamlTypeInfo` forms. - `YamlSerializerOptions` and the enums, `ReflectionYamlTypeInfoResolver`, `YamlTypeInfo`/`YamlTypeInfo`. - Attributes: `YamlPropertyName`, `YamlIgnore`, `YamlInclude`, `YamlRequired`, `YamlPropertyOrder`, `YamlConstructor`, `YamlBlockSequenceItemStyle`, `YamlSerializable` (accepted so code compiles); `System.Text.Json` attributes are the fallback. - `YamlException`, `SyntaxErrorException`, `SemanticErrorException`, `Mark`. - Constructors, records, init-only and required members, enums, nullable types, collections, dictionaries, `object`. ## What is left out A program that uses these fails to compile (so the gap is visible), except the options listed under "Fail at first use". - Custom converters: `YamlConverter`, `YamlReader`, `YamlWriter`, `YamlSerializerOptions.Converters`, `[YamlConverter]`. - Serialization hooks (`IYamlOnSerializing` and the other `IYamlOn*` interfaces). - Polymorphism: `[YamlPolymorphic]`, `[YamlDerivedType]`, `PolymorphismOptions`. Extension data: `[YamlExtensionData]`. - The source-generated context shape: `[YamlSerializable] partial class Ctx : YamlSerializerContext` compiles, but there is no generated `Ctx.Default` or typed property. Use the reflection path, or `OneYaml`'s own generator. - `Model.*` (`YamlStream`, `YamlDocument`, nodes), `Syntax.*`, `Schemas`, scanner, parser, emitter. Fail at first use with `NotSupportedException` (naming the option): `Schema`/`UseSchema` other than Core, `DuplicateKeyHandling` other than the default error, `DictionaryKeyPolicy`, `PreferredObjectCreationHandling.Populate`, `DefaultIgnoreCondition` of `Always`/`WhenWriting`/`WhenReading`, `UnsafeAllowDeserializeFromTagTypeName`, and a custom `TypeInfoResolver`. The writer has one layout, so the layout options are refused the same way unless they ask for that layout: `BlockSequenceMappingStyle` other than `Default` or `Compact`, `BlockSequenceSequenceStyle` other than `Default` or `Expanded`, `ScalarStylePreferences` with `PreferPlainStyle` or `PreferQuotedForAmbiguousScalars` set, and `WriteIndented = false`. `ReferenceHandling` other than `None` is refused when writing, because the writer writes a shared object in full each time and writes no anchors; reading follows aliases with any value. ## Added, not in SharpYaml - `YamlSerializerOptions.Limits` (`OneYaml.YamlLimits?`): the limits every read through the options keeps to. `null`, the default, means `YamlLimits.Default`; set `YamlLimits.Untrusted` for input you do not control. `MaxDepth` can lower the depth further, never raise it. Code that sets it does not compile against SharpYaml. Import the type with `using YamlLimits = OneYaml.YamlLimits;`, not `using OneYaml;`: that namespace has its own `YamlSerializer`, `YamlSerializerOptions` and `YamlException`, so importing it beside `OneYaml.Compat.SharpYaml` makes those names ambiguous (error CS0104). ## Fixes | Difference | Repro | |---|---| | Empty or comment-only text read into a value type throws `NullReferenceException` in the real library; here it gives the default value, as a null member does | `Deserialize("")` and `Deserialize("# note")` give 0 | | `Uri` cannot be read or written by the real library | `Deserialize("http://x/y")` | | The real library rejects about 70 valid YAML 1.2 documents (a bare document after `...`, `?x` as a plain scalar, anchor names containing `:`, and others) | yts `2EBW`-style cases; see `CorpusTests` | | Block scalars that keep trailing lines lose or gain a line break in the real library | `\|+\n a\n\n` (`JEF9/02`, `L24T/01`) | | Plain scalars that start or end in an indicator are misread by the real library | `652Z`, `4ABK`, `HM87/01` | | A line that starts with `%` after the first line of a document, with another document following, is part of the first document. The real library fails on a block scalar that holds one | `Deserialize("--- \|\nfoo\n%bar\n---\nx\n")` gives `"foo\n%bar\n"`; `"a\n%x\n%y\n---\nb\n"` gives `"a %x %y"` | | A stream with bytes that are not valid in its encoding ends in a `SyntaxErrorException`; the real library throws a `DecoderFallbackException` (not a `YamlException`) for UTF-8 input, and for UTF-16 or UTF-32 input with a byte order mark it silently replaces the bad bytes with U+FFFD. The error mark counts characters of the detected encoding | `Deserialize(stream)` with a stray `0xFF` byte | | Aliases into `object` are refused by the real library unless `ReferenceHandling` is `Preserve`; here they are followed within the alias budget | `a: &x [1]\nb: *x` into `object` | One `%` case is kept as SharpYaml 3 has it, not as the YAML specification says. A `%` line straight after a block collection, before the next `---`, is taken as a directive of the next document, so `Deserialize>("a: 1\n%TAG ! tag:example.com,2000:\n---\nb: 2\n")` gives `{a: 1}` in both. The specification requires a `...` document end marker before a directive, and the `OneYaml` API refuses this text without one. The adapter keeps SharpYaml's reading because it changes no data. ## Intended differences | Difference | Repro | |---|---| | YAML 1.2 Core only: `1_000` and `0b11` are text, not numbers (the real library reads them as numbers) | `Deserialize("1_000")` throws here (real: 1000) | | A hexadecimal integer is accepted into a `double` member, and into `Int128`, `UInt128` and `Half` (the real library refuses it for these three; YAML 1.2 reads `0x10` as an integer, as for `long`) | `Deserialize("Score: 0x10")`; `Deserialize("0x10")` | | A very large integer in an `object` read is a `BigInteger` (real: `double`) | `Deserialize("123456789012345678901234567890")` | | `Half` infinity and NaN are written as `.inf`, `-.inf` and `.nan`, the YAML 1.2 floats that `double` gets in both libraries (the real library writes `Infinity`, `-Infinity` and `NaN`, plain strings to any other reader), and `.inf`, `-.inf` and `.nan` are read into `Half` (the real library refuses them). The spellings the real library reads, `Infinity`, `-Infinity` and `NaN` in any letter case and with a sign, are read into `double`, `float` and `Half`, quoted or not | `Serialize(Half.PositiveInfinity)` gives `.inf\n`; `Deserialize(".nan")` is NaN | | Whole doubles and halves are written with a fraction so they read back as floats, and an exponent form carries one too | `Serialize(100.0)` gives `100.0` (real: `100`); `Serialize(Half.Epsilon)` gives `6.0E-08` (real: `6E-08`) | | 1.1 look-alikes are quoted on output so any reader sees a string | `Serialize("y")` gives `'y'`; also `'Y'`, `'on'` | | Member order is base class first, fields before properties; `Sorted` order applies to members only | `Serialize(new Derived())` | | Quoted `"5"` and `"true"` are read into numbers and booleans (kept from the real library's lenient reads) | `Deserialize("\"5\"")` gives 5 | | A number whose digits overflow the type is an error, quoted or not; the real library turns it into infinity, which changes the value. Plain `.inf` and `-.inf` still read as infinity (a quoted `".inf"` is refused; the real library reads it as infinity) | `Deserialize("1e400")`, `Deserialize("1e39")` and `Deserialize("1e10")` throw here (real: `∞`) | | A stream in UTF-16 or UTF-32 without a byte order mark is detected from its first characters, as YAML 1.2 says (the real library fails) | `Deserialize(stream)` over `a: b` in UTF-16 | | A null inside a mapping or sequence, read into a non-nullable number or boolean, is the default value (real: `YamlException` for a mapping value or sequence item). A null that is the whole document is an error here too, as in the real library | `Deserialize>("[~]")` gives `[0]`; `Deserialize("~")` throws | | Enums are written by name regardless of the naming policy | `Serialize(Mood.Happy, new() { PropertyNamingPolicy = CamelCase })` | | `MaxDepth` defaults to 64 as in the real library and is never raised above the depth limit of `Limits`; the property reads back the value set, and the lower of the two applies | `YamlSerializerOptions { MaxDepth = 100000 }` reads back 100000 and still stops at the limit | | Error marks are a point (`Start == End`), normally at the real library's line and column but not always | `Deserialize("a: [")` | | A leading byte order mark (U+FEFF) is never counted in error marks, so a text gets the marks it would have without the mark from a string, a span, a `TextReader` or a `Stream`. The real library counts the mark as a character and a column when the text is a string or a `TextReader`, but not when it reads a `Stream`, so the same text gets different marks from different inputs | `Deserialize("\uFEFFa: 1\nb: [x\n")`: `Index` 12 in the real library, 11 here, as for the same text read from a stream | | Syntax errors are `SyntaxErrorException`; every other category is `YamlException` (the real `SemanticErrorException` is never raised) | `Deserialize("a: [")` is a syntax error; `Deserialize("x")` is a `YamlException` | | `Stream` input is read to the end, or until it passes `Limits.MaxInputBytes`, with UTF-8, UTF-16 and UTF-32 detected, and left open | `Deserialize(stream)` | | `BlockSequenceMappingStyle`, `BlockSequenceSequenceStyle`, `ScalarStylePreferences` and `WriteIndented` are accepted at their default values and refused at any value that asks for another layout | `Serialize(x, new() { BlockSequenceMappingStyle = YamlSequenceItemStyle.Expanded })` throws `NotSupportedException` | | `GetTypeInfo` returns a nullable `YamlTypeInfo` | | | Only the first document is read, as in the real library; the rest of the stream is not parsed unless the first fails. `Limits.MaxInputBytes` still applies to the whole text, every document included | `Deserialize("a: 1\n---\n[")` | ## Gaps - `DuplicateKeyHandling.LastWins` and `FirstWins` (the engine has no option for them). - Source-generated contexts (see "What is left out"). - `ReferenceHandling.Preserve` on write: refused with `NotSupportedException` (see "Fail at first use"), as the writer writes no anchors. - Error text differs from the real library's wording; marks follow the rule above. # Comparison How oneyaml-csharp compares with the .NET YAML libraries it is most often weighed against: YamlDotNet, SharpYaml and VYaml. **Bias.** This page is written by the authors of oneyaml-csharp. We chose the criteria, and they are the ones this library was built for: YAML 1.2 behaviour, editing a file without disturbing it, and bounded handling of untrusted input. A project with other priorities would draw a different table. We tried to state each library's strengths as its own users would. **Versions.** This page describes YamlDotNet 18.1.0, SharpYaml 3.14.0 and VYaml 1.4.0. Library facts come from the libraries' own documentation and from a survey we ran against those versions on 2026-09-30. On 2026-10-16 the current releases were YamlDotNet 18.1.0, SharpYaml 3.16.0 and VYaml 1.4.1; we read the release notes and READMEs of the newer two and found nothing that changes a line here, but we did not repeat the behaviour tests (depth limits, aliases) against them. Libraries change, so check the current release before you rely on a line here. This page compares features and behaviour and has no figures. Speed and memory are in [Performance](https://recursiveai.org/oneyaml/csharp/docs/performance/), generated from the kit's release run, whose method line names the library versions it measured; these can be newer than the ones above. Conformance counts are in [Conformance](https://recursiveai.org/oneyaml/csharp/docs/conformance/). ## What each library is | | oneyaml-csharp | YamlDotNet | SharpYaml 3 | VYaml | |---|---|---|---|---| | API shape | System.Text.Json style serializer, plus a lossless `YamlDocument` | builders (`DeserializerBuilder`, `SerializerBuilder`) and a representation model (`YamlStream`) | System.Text.Json style serializer, a syntax tree and a model | `[YamlObject]` source-generated serializer and a UTF-8 parser | | Default YAML version | 1.2, Core schema | a mix of 1.1 and 1.2; typed binding follows 1.1 (`yes` is `true`, `017` is 15) | 1.2, Core schema by default; other schemas selectable | 1.2, "mostly supported" by its own description | | Targets | .NET 8, .NET 10 | `netstandard2.0` and up, .NET Framework, current .NET | `netstandard2.0` and up, current .NET | Unity first, also NuGet | | Status | new; 1.0.0 is the first release | the most used .NET YAML library, maintained for many years | maintained; v3 was a breaking redesign in 2026 | maintained, Unity-focused | ## Like with like Compare libraries on the job they share. - **Typed load and dump.** All four. oneyaml-csharp and SharpYaml share the System.Text.Json shape (options object, naming policies, System.Text.Json attributes, a source-generated context); YamlDotNet uses builders and its own attributes; VYaml generates code for `[YamlObject]` types. All four have a source generator for native AOT; YamlDotNet's is a separate package. - **Editing a file and keeping it as it was.** oneyaml-csharp's `YamlDocument` writes a new value in the old one's style and keeps every other byte. SharpYaml's `YamlSyntaxTree.WithTextChange` also keeps every byte outside the range you replace, but you write the YAML text of the new value yourself. YamlDotNet's `YamlStream` and SharpYaml's model keep a scalar's style but lose comments and blank lines when they save. VYaml has no document model. - **Patching.** oneyaml-csharp applies RFC 6902 and RFC 7396 to the YAML text, atomically. None of the others has a patch API; the usual route is to convert to a JSON tree (for example with Yaml2JsonNode and JsonPatch.Net), which loses formatting and comments. - **Limits on input.** oneyaml-csharp bounds depth, alias expansion and integer size by default and has a `YamlLimits.Untrusted` preset ([Limits](https://recursiveai.org/oneyaml/csharp/docs/limits/)). SharpYaml limits depth (64 by default) and refuses aliases into untyped targets unless you opt in. YamlDotNet's deserializer limits recursion; in our survey its `YamlStream` 18.1.0 had no depth limit, and VYaml had none either: deep input ended the process with a stack overflow. ## Where oneyaml-csharp is the better fit - You change YAML files that people also edit by hand, and the diff must hold only your change. - You want to apply a JSON Patch or a merge patch to a YAML file and keep its comments. - You want YAML 1.2 as the specification says, measured against the yaml-test-suite with every known deviation listed ([Conformance](https://recursiveai.org/oneyaml/csharp/docs/conformance/)). - You read YAML from outside your program and want bounds on by default, with a stricter preset to opt into. - You already know System.Text.Json and want the same options, attributes and source generation for YAML. ## Where the others are the better fit - **You target .NET Framework, .NET Standard or Unity.** oneyaml-csharp runs on .NET 8 and .NET 10 only. YamlDotNet and SharpYaml target `netstandard2.0`; VYaml is built for Unity. - **You need YamlDotNet's extension points.** Type inspectors, node deserializers, event emitters and object-graph visitors let you change how YamlDotNet binds at a level the oneyaml-csharp serializer does not open. The YamlDotNet adapter does not provide them either, and the SharpYaml adapter has no custom converters; each migration guide lists what its adapter is missing. - **You want maturity and an ecosystem.** YamlDotNet has years of production use, a large number of dependent packages and integrations (configuration providers, for example). oneyaml-csharp is new, and 1.0.0 is its first release. - **You read or write YAML 1.1 files as YAML 1.1.** YamlDotNet reads `yes` as `true` and `017` as octal in typed binding by default. In oneyaml-csharp that takes the YamlDotNet adapter. - **You need comments as data.** No library here gives a full comment API, but YamlDotNet's parser can report comment events and its emitter can write them. oneyaml-csharp keeps comments through edits but cannot read or add them. - **You need a streaming event parser.** YamlDotNet's `Parser`, SharpYaml's parser and VYaml's `YamlParser` are public pull parsers. oneyaml-csharp exposes its reader only inside a converter. - **You need SharpYaml's schemas.** SharpYaml can select the Failsafe, JSON, Core or an extended schema. oneyaml-csharp reads with the Core schema only. - **Speed on a given workload.** See [Performance](https://recursiveai.org/oneyaml/csharp/docs/performance/) for where oneyaml-csharp is slower; a library that wins on your workload may be the better choice. ## Choosing - New code on .NET 8 or later that reads configuration, or edits files people maintain: oneyaml-csharp. - Existing YamlDotNet or SharpYaml code: keep it, or swap to the adapter and move to the `OneYaml` API file by file ([from YamlDotNet](https://recursiveai.org/oneyaml/csharp/docs/migrate-yamldotnet/), [from SharpYaml](https://recursiveai.org/oneyaml/csharp/docs/migrate-sharpyaml/)). - .NET Framework or Unity: YamlDotNet, SharpYaml or VYaml. # Performance How oneyaml-csharp compares with the other .NET YAML libraries on speed and memory. Every task runs on the same inputs for every library, through the oneyaml benchmark kit, [oneyaml-kit](https://github.com/recursive-ai-oss/oneyaml-kit). The tables and charts below are generated from the kit's result file; none of the numbers is typed by hand. The figures are measured, not bounds: they hold for the workloads, versions and host named in the method line of each block. A run that is marked as not publishable is a smoke result; it shows the layout and the rough picture, and it is not a claim. What is compared: - **Loading and writing**, typed and untyped, against YamlDotNet, SharpYaml and VYaml. oneyaml-csharp is measured on both its reflection path and its generated-metadata path. - **Reading a document for editing**, and **editing** it, against the closest .NET equivalents, such as SharpYaml's syntax tree. - **Applying a patch** against Yaml2JsonNode with JsonPatch.Net, the usual .NET route for a JSON Patch on YAML. That route converts to a JSON tree and does not keep the file's formatting, so the comparison is of the work, not of equal output. Each generated table names the competitors of every task, and a cell without a number says why. The kit's [benchmark notes](https://github.com/recursive-ai-oss/oneyaml-kit/blob/main/docs/benchmarks.md#tasks) define each task and how it is timed. The kit also decides when a difference in time or memory is too small to call a win or a loss; the [verdict rules](https://github.com/recursive-ai-oss/oneyaml-kit/blob/main/docs/benchmarks.md#verdicts) say where those bands lie and how a verdict is reached. ## Results at a glance One row per task: our time and the strongest competitor's, each the geometric mean over the task's workloads, with the verdict and the method of the run. The chart in the README is drawn from these rows. Bar chart of the table below: how many times faster oneyaml-csharp is than the strongest competitor on each task, log scale, higher is better. 7 tasks: 5 better, 0 level, 1 mixed, 0 worse, 1 not comparable. Speeds are over real-world workloads. A mark counts every workload. Loses only outside the real-world workloads: unchanged emit. | Task | `oneyaml-csharp` time (geometric mean) | Strongest competitor | Its time (geometric mean) | Speed against it (its time ÷ ours; higher is better) | Verdict | |---|---|---|---|---|---| | typed load | 47.5 µs | `vyaml 1.4.1` | 60.7 µs | 1.28× faster | ▲ win | | generic load | 22.2 µs | `vyaml 1.4.1` | 29.5 µs | 1.33× faster | ▲ win | | dump | 4.77 µs | `sharpyaml 3.16.0` (4 of 6), `vyaml 1.4.1` (2 of 6) | 12.6 µs | 2.63× faster | ▲ win | | document parse | 17.8 µs | `sharpyaml-syntax 3.16.0` † (sharpyaml-syntax: bytes-only) | 193 µs | 10.8× faster | ▲ win | | unchanged emit (write-back of an unedited document; not a rendering speed) | 504 ns | `sharpyaml-syntax 3.16.0` | 1.25 µs | 2.48× faster | ◆ mixed (loses 2 of 20 generated workloads) | | edit script | 7.48 µs | `sharpyaml-syntax 3.16.0` † (sharpyaml-syntax: harness-rendered) | 554 µs | 74.0× faster | ▲ win | | patch (RFC 6902) | — | — | — | — | no rival | Not in a row's mean: patch (RFC 6902): no competitor answer: `compose-wordpress`, `gha-workflow`. Each row is the geometric mean, over the real-world workloads the task has, of our time and of the time of the strongest competitor in each workload. Speed is the competitor's time divided by ours: 2× faster means half the time, and a loss is stated as slower. Where several competitors were strongest the column names them all, with how often. A † marks a cell measured differently from the task as specified; the note beside each mark says how. A marked competitor still counts. A workload where our library has no number is listed and the row is marked as partial, never counted as a win. The verdict is never better than the per-workload verdicts allow: a row with a losing workload is marked mixed or loss, and a row with no winning workload is never marked a win. The speed is over the real-world workloads. The verdict is never better than the real-world workloads allow, nor better than all of the task's workloads allow, generated and reference ones included: a loss on any of them keeps a row from being a clean win and the row says how many it lost, and a win on them cannot lift a row the real-world workloads do not win. Memory and the count of better, level and worse workloads in each row are on the full page. Method: **full run** of 2026-10-18T00:48:41Z. Kit 0.1.0 (revision 6cc83d12fbc6); text generated by kit revision c1647a46aa6d. `oneyaml-csharp c23a19250217`; competitors `yamldotnet 18.1.0`, `sharpyaml 3.16.0`, `sharpyaml-model 3.16.0`, `sharpyaml-syntax 3.16.0`, `vyaml 1.4.1`, `yaml2jsonnode 3.0.1`, `oneyaml-csharp-gen c23a19250217`. Runtime .NET 10.0.11 (target linux-x64), measured with BenchmarkDotNet 0.15.8: mean time per task from BenchmarkDotNet's ShortRun job (3 warm-up and 3 measured iterations) run in process; one invocation per iteration runs a batch of prepared tasks and the time is divided by the batch size; every cell is measured in a process of its own, because the heap that earlier cells leave behind changed the result. Each process runs with DOTNET_TC_CallCountingDelayMs=0 (call counting starts immediately instead of after the default delay), and no other DOTNET_ or COMPlus_ variable set, and every cell is first warmed up for 1000 ms of whole batches before BenchmarkDotNet's own warm-up, because a new process starts at the runtime's unoptimised tier. Memory: bytes allocated per task from BenchmarkDotNet's MemoryDiagnoser; allocation calls are not available. AMD Ryzen 9 7950X3D 16-Core Processor (125.0 GiB), Linux 7.0.0-34-generic (x86_64), measured on a shared workstation (32 logical CPUs; load 3.72 before, 4.52 after, at most 8.48 during). ## Results Every task and workload, time and memory, from the same run. **Time per operation** | Task | Workload | Input | `oneyaml-csharp` | `yamldotnet` | `sharpyaml` | `sharpyaml-model` | `sharpyaml-syntax` | `vyaml` | `yaml2jsonnode` | `oneyaml-csharp-gen` | Ours vs strongest competitor | |---|---|---|---|---|---|---|---|---|---|---|---| | typed load | `gha-workflow` | 9.43 KiB | 47.5 µs | 355 µs | 139 µs | unsupported | unsupported | 60.7 µs | 247 µs | 47.3 µs | 0.78× of `vyaml` ▲ win | | typed load | `records-4k` | 4.14 KiB | 33.9 µs | 281 µs | 102 µs | unsupported | unsupported | 42.9 µs | 193 µs | 33.7 µs | 0.79× of `vyaml` ▲ win | | typed load | `records-64k` | 64.2 KiB | 561 µs | 4.59 ms | 1.66 ms | unsupported | unsupported | 712 µs | 3.13 ms | 563 µs | 0.79× of `vyaml` ▲ win | | typed load | `records-1m` | 1 MiB | 8.56 ms | 72.7 ms | 27.4 ms | unsupported | unsupported | 10.4 ms | 93.7 ms | 8.49 ms | 0.83× of `vyaml` ▲ win | | typed load | `records-8m` | 8 MiB | 70.2 ms | 668 ms | 261 ms | unsupported | unsupported | 85.1 ms | 983 ms | 67.2 ms | 0.83× of `vyaml` ▲ win | | generic load | `compose-wordpress` | 815 B | 3.98 µs | 29.8 µs | 13.9 µs | 11.1 µs | unsupported | 5.1 µs | 17.2 µs | 4.12 µs | 0.78× of `vyaml` ▲ win | | generic load | `gha-workflow` | 9.43 KiB | 50.9 µs | 337 µs | 146 µs | 140 µs | unsupported | 70.9 µs | 239 µs | 49.6 µs | 0.72× of `vyaml` ▲ win | | generic load | `helm-ingress-nginx` | 52.4 KiB | 73.3 µs | 692 µs | 334 µs | 325 µs | unsupported | 103 µs | 545 µs | 73.5 µs | 0.71× of `vyaml` ▲ win | | generic load | `k8s-ingress-nginx-multi` | 16 KiB | 103 µs | 776 µs | unsupported | 292 µs | unsupported | 131 µs | 459 µs | 101 µs | 0.79× of `vyaml` ▲ win | | generic load | `openapi-api-with-examples` | 6.75 KiB | 20.8 µs | 141 µs | 60.8 µs | 61.1 µs | unsupported | 25.9 µs | 95.6 µs | 20.4 µs | 0.80× of `vyaml` ▲ win | | generic load | `openapi-petstore` | 5.34 KiB | 25.1 µs | 191 µs | 80.5 µs | 74.2 µs | unsupported | 38.6 µs | 115 µs | 28.8 µs | 0.65× of `vyaml` ▲ win | | generic load | `prometheus-config` | 1.1 KiB | 3.37 µs | 25.2 µs | 10.1 µs | 10.1 µs | unsupported | 3.97 µs | 15.6 µs | 3.36 µs | 0.85× of `vyaml` ▲ win | | generic load | `reference-a` | 51 B | 617 ns | 4.94 µs | 2.04 µs | 1.99 µs | unsupported | 739 ns | 3.94 µs | 1.11 µs | 0.83× of `vyaml` ▲ win | | generic load | `reference-b` | 51 B | 637 ns | 4.67 µs | 2 µs | 1.9 µs | unsupported | 739 ns | 3.24 µs | 609 ns | 0.86× of `vyaml` ▲ win | | generic load | `records-4k` | 4.14 KiB | 43.7 µs | 284 µs | 111 µs | 103 µs | unsupported | 55.2 µs | 166 µs | 44.3 µs | 0.79× of `vyaml` ▲ win | | generic load | `records-64k` | 64.2 KiB | 640 µs | 4.41 ms | 1.81 ms | 1.79 ms | unsupported | 897 µs | 2.93 ms | 640 µs | 0.71× of `vyaml` ▲ win | | generic load | `records-1m` | 1 MiB | 10.7 ms | 84.8 ms | 29.5 ms | 32.8 ms | unsupported | 13.7 ms | 76.8 ms | 10 ms | 0.78× of `vyaml` ▲ win | | generic load | `records-8m` | 8 MiB | 99.6 ms | 773 ms | 434 ms | 589 ms | unsupported | 134 ms | 910 ms | 110 ms | 0.75× of `vyaml` ▲ win | | generic load | `compose-merge-2k` | 2.14 KiB | 18.7 µs | 138 µs | 57.3 µs (merged) | 62.6 µs | unsupported | 27.5 µs | 87.4 µs | 19.1 µs | 0.68× of `vyaml` ▲ win | | generic load | `compose-merge-64k` | 64.1 KiB | 556 µs | 3.79 ms | 1.73 ms (merged) | 1.81 ms | unsupported | 769 µs | 2.67 ms | 508 µs | 0.72× of `vyaml` ▲ win | | generic load | `compose-merge-1m` | 1 MiB | 8.14 ms | 78.3 ms | 41.3 ms (merged) | 56.2 ms | unsupported | 12.3 ms | 95.6 ms | 8.28 ms | 0.66× of `vyaml` ▲ win | | generic load | `k8s-multi-16k` | 16.4 KiB | 177 µs | 1.16 ms | unsupported | 455 µs | unsupported | 212 µs | 719 µs | 169 µs | 0.83× of `vyaml` ▲ win | | generic load | `k8s-multi-1m` | 1 MiB | 10.2 ms | 110 ms | unsupported | 33.2 ms | unsupported | 14.5 ms | 89.9 ms | 11.1 ms | 0.70× of `vyaml` ▲ win | | generic load | `openapi-100k` | 101 KiB | 803 µs | 5.38 ms | 2.21 ms | 2.23 ms | unsupported | 1.01 ms | 3.64 ms | 881 µs | 0.79× of `vyaml` ▲ win | | generic load | `openapi-8m` | 8 MiB | 60.5 ms | 603 ms | 312 ms | 453 ms | unsupported | 105 ms | 701 ms | 60.1 ms | 0.58× of `vyaml` ▲ win | | generic load | `helm-values-64k` | 64 KiB | 347 µs | 2.54 ms | 1.11 ms | 1.06 ms | unsupported | 470 µs | 1.72 ms | 338 µs | 0.74× of `vyaml` ▲ win | | generic load | `helm-values-1m` | 1 MiB | 5.46 ms | 56.3 ms | 25.8 ms | 22 ms | unsupported | 7.23 ms | 55 ms | 6.21 ms | 0.76× of `vyaml` ▲ win | | generic load | `deep-block` | 5.23 KiB | 16.3 µs | 116 µs | rejects [1] | rejects [6] | unsupported | 18.3 µs | 65.8 µs | 16.2 µs | 0.89× of `vyaml` ▲ win | | generic load | `deep-flow` | 202 B | 8.93 µs | 67.3 µs | rejects [1] | rejects [6] | unsupported | 22 µs | 38.3 µs | 9.62 µs | 0.41× of `vyaml` ▲ win | | generic load | `wide-map-64k` | 64 KiB | 644 µs | 4.56 ms | 2.01 ms | 2.05 ms | unsupported | 811 µs | 2.9 ms | 610 µs | 0.79× of `vyaml` ▲ win | | generic load | `wide-map-1m` | 1 MiB | 10.9 ms | 79.2 ms | 39.7 ms | 40.4 ms | unsupported | 14.1 ms | 87.9 ms | 11.1 ms | 0.77× of `vyaml` ▲ win | | generic load | `long-scalars-256k` | 261 KiB | 322 µs | 2.41 ms | 2.1 ms | 2.24 ms | unsupported | 1.35 ms | 2.34 ms | 308 µs | 0.24× of `vyaml` ▲ win | | generic load | `flow-heavy-64k` | 64 KiB | 1.12 ms | 7.51 ms | 2.95 ms | 2.67 ms | unsupported | 1.31 ms | 4.73 ms | 1.09 ms | 0.85× of `vyaml` ▲ win | | generic load | `anchors-32k` | 32 KiB | 381 µs | 2.14 ms | 907 µs | 1.84 ms | unsupported | 572 µs | 2.08 ms | 384 µs | 0.67× of `vyaml` ▲ win | | generic load | `unicode-32k` | 32 KiB | 156 µs | 811 µs | 391 µs | 372 µs | unsupported | 246 µs | 554 µs | 177 µs | 0.63× of `vyaml` ▲ win | | generic load | `scalar-styles-32k` | 32 KiB | 218 µs | 1.28 ms | 588 µs | 599 µs | unsupported | 320 µs | 824 µs | 234 µs | 0.68× of `vyaml` ▲ win | | dump | `compose-wordpress` | 815 B | 1.33 µs | 23.6 µs | 4.8 µs | 15.5 µs | unsupported | 5.29 µs | 53.9 µs | 1.31 µs | 0.28× of `sharpyaml` ▲ win | | dump | `gha-workflow` | 9.43 KiB | 12.5 µs | 255 µs | 32.7 µs | differs [5] | unsupported | 27 µs | 217 µs | 12.7 µs | 0.46× of `vyaml` ▲ win | | dump | `helm-ingress-nginx` | 52.4 KiB | 17.7 µs | 351 µs | 31.8 µs | 251 µs | unsupported | differs [11] | 253 µs | 18.4 µs | 0.56× of `sharpyaml` ▲ win | | dump | `openapi-api-with-examples` | 6.75 KiB | 5.9 µs | 111 µs | 13.3 µs | 74.7 µs | unsupported | 14.6 µs | 117 µs | 6.62 µs | 0.44× of `sharpyaml` ▲ win | | dump | `openapi-petstore` | 5.34 KiB | 8.41 µs | 155 µs | 21.8 µs | 139 µs | unsupported | 20.2 µs | 147 µs | 8.8 µs | 0.42× of `vyaml` ▲ win | | dump | `prometheus-config` | 1.1 KiB | 816 ns | 19.3 µs | 3.55 µs | 9.68 µs | unsupported | 4.18 µs | 48.5 µs | 837 ns | 0.23× of `sharpyaml` ▲ win | | dump | `reference-a` | 51 B | 239 ns | 4.52 µs | 1.46 µs | 1.14 µs | unsupported | differs [9] | 42.5 µs | 242 ns | 0.21× of `sharpyaml-model` ▲ win | | dump | `reference-b` | 51 B | 241 ns | 4.62 µs | 1.74 µs | 1.38 µs | unsupported | differs [9] | 41.1 µs | 238 ns | 0.18× of `sharpyaml-model` ▲ win | | dump | `records-4k` | 4.14 KiB | 13.4 µs | differs [14] | differs [2] | 121 µs | unsupported | differs [10] | 154 µs | 13.6 µs | 0.11× of `sharpyaml-model` ▲ win | | dump | `records-64k` | 64.2 KiB | 174 µs | differs [14] | differs [2] | 1.99 ms | unsupported | differs [10] | 1.89 ms | 168 µs | 0.09× of `yaml2jsonnode` ▲ win | | dump | `records-1m` | 1 MiB | 3.11 ms | differs [14] | differs [2] | 33.4 ms | unsupported | differs [10] | 41.1 ms | 3.28 ms | 0.09× of `sharpyaml-model` ▲ win | | dump | `records-8m` | 8 MiB | 30.4 ms | differs [14] | differs [2] | 285 ms | unsupported | differs [10] | 582 ms | 26.5 ms | 0.11× of `sharpyaml-model` ▲ win | | dump | `compose-merge-2k` | 2.14 KiB | 9.01 µs | 109 µs | 18.6 µs (merged) | 105 µs | unsupported | 23.2 µs | 140 µs | 9.34 µs | 0.48× of `sharpyaml` ▲ win | | dump | `compose-merge-64k` | 64.1 KiB | 265 µs | 2.95 ms | 628 µs (merged) | 2.49 ms | unsupported | 533 µs | 3.07 ms | 270 µs | 0.50× of `vyaml` ▲ win | | dump | `compose-merge-1m` | 1 MiB | 4.3 ms | 51 ms | 11.4 ms (merged) | 52 ms | unsupported | 8.98 ms | 88.8 ms | 4.39 ms | 0.48× of `vyaml` ▲ win | | dump | `openapi-100k` | 101 KiB | 231 µs | 4.29 ms | 525 µs | 1.82 ms | unsupported | 439 µs | 2.45 ms | 234 µs | 0.53× of `vyaml` ▲ win | | dump | `openapi-8m` | 8 MiB | 21.9 ms | 384 ms | 55.2 ms | 228 ms | unsupported | 39.3 ms | 480 ms | 21.6 ms | 0.56× of `vyaml` ▲ win | | dump | `helm-values-64k` | 64 KiB | 72.6 µs | 1.57 ms | 156 µs | 1.18 ms | unsupported | 180 µs | 1.19 ms | 69.9 µs | 0.47× of `sharpyaml` ▲ win | | dump | `helm-values-1m` | 1 MiB | 1.22 ms | 27.8 ms | 2.78 ms | 20.2 ms | unsupported | 2.88 ms | 24.2 ms | 1.33 ms | 0.44× of `sharpyaml` ▲ win | | dump | `deep-block` | 5.23 KiB | 4.52 µs | rejects [13] | rejects [1] | rejects [6] | unsupported | 11.2 µs | 95.4 µs | 4.48 µs | 0.40× of `vyaml` ▲ win | | dump | `deep-flow` | 202 B | 2.18 µs | rejects [13] | rejects [1] | rejects [6] | unsupported | 7.17 µs | 56.7 µs | 2.23 µs | 0.30× of `vyaml` ▲ win | | dump | `wide-map-64k` | 64 KiB | 159 µs | 2.29 ms | 290 µs | 2.11 ms | unsupported | 290 µs | 2.44 ms | 159 µs | 0.55× of `vyaml` ▲ win | | dump | `wide-map-1m` | 1 MiB | 1.97 ms | 33.8 ms | 4.72 ms | 32.2 ms | unsupported | 3.95 ms | 33.7 ms | 1.97 ms | 0.50× of `vyaml` ▲ win | | dump | `long-scalars-256k` | 261 KiB | 110 µs | 4.03 ms | 719 µs | differs [4] | unsupported | differs [8] | 3.66 ms | 122 µs | 0.15× of `sharpyaml` ▲ win | | dump | `flow-heavy-64k` | 64 KiB | 245 µs | 5.89 ms | 617 µs | 1.94 ms | unsupported | 571 µs | 2.73 ms | 243 µs | 0.43× of `vyaml` ▲ win | | dump | `anchors-32k` | 32 KiB | 418 µs | 1.18 ms | 849 µs | 3.22 ms | unsupported | 872 µs | 4.43 ms | 428 µs | 0.49× of `sharpyaml` ▲ win | | dump | `unicode-32k` | 32 KiB | 47 µs | 584 µs | 112 µs | differs [3] | unsupported | 50.6 µs | 416 µs | 48.7 µs | 0.93× of `vyaml` ▲ win | | dump | `scalar-styles-32k` | 32 KiB | 71.1 µs | 923 µs | 123 µs | 723 µs | unsupported | differs [9] | 603 µs | 70.4 µs | 0.58× of `sharpyaml` ▲ win | | document parse | `compose-wordpress` | 815 B | 4.6 µs | unsupported | unsupported | unsupported | 29.9 µs (bytes-only) † | unsupported | unsupported | 2.73 µs | 0.15× of `sharpyaml-syntax` † (bytes-only) ▲ win | | document parse | `gha-workflow` | 9.43 KiB | 36 µs | unsupported | unsupported | unsupported | 431 µs (bytes-only) † | unsupported | unsupported | 40.3 µs | 0.08× of `sharpyaml-syntax` † (bytes-only) ▲ win | | document parse | `helm-ingress-nginx` | 52.4 KiB | 64.5 µs | unsupported | unsupported | unsupported | 1.14 ms (bytes-only) † | unsupported | unsupported | 61.8 µs | 0.06× of `sharpyaml-syntax` † (bytes-only) ▲ win | | document parse | `k8s-ingress-nginx-multi` | 16 KiB | 72.8 µs | unsupported | unsupported | unsupported | 858 µs (bytes-only) † | unsupported | unsupported | 78.2 µs | 0.08× of `sharpyaml-syntax` † (bytes-only) ▲ win | | document parse | `openapi-api-with-examples` | 6.75 KiB | 16.8 µs | unsupported | unsupported | unsupported | 151 µs (bytes-only) † | unsupported | unsupported | 14.9 µs | 0.11× of `sharpyaml-syntax` † (bytes-only) ▲ win | | document parse | `openapi-petstore` | 5.34 KiB | 18 µs | unsupported | unsupported | unsupported | 193 µs (bytes-only) † | unsupported | unsupported | 17.7 µs | 0.09× of `sharpyaml-syntax` † (bytes-only) ▲ win | | document parse | `prometheus-config` | 1.1 KiB | 2.4 µs | unsupported | unsupported | unsupported | 26.9 µs (bytes-only) † | unsupported | unsupported | 2.35 µs | 0.09× of `sharpyaml-syntax` † (bytes-only) ▲ win | | document parse | `reference-a` | 51 B | 619 ns | unsupported | unsupported | unsupported | 4.5 µs (bytes-only) † | unsupported | unsupported | 610 ns | 0.14× of `sharpyaml-syntax` † (bytes-only) ▲ win | | document parse | `reference-b` | 51 B | 594 ns | unsupported | unsupported | unsupported | 4.3 µs (bytes-only) † | unsupported | unsupported | 609 ns | 0.14× of `sharpyaml-syntax` † (bytes-only) ▲ win | | document parse | `records-4k` | 4.14 KiB | 30.8 µs | unsupported | unsupported | unsupported | 271 µs (bytes-only) † | unsupported | unsupported | 30.8 µs | 0.11× of `sharpyaml-syntax` † (bytes-only) ▲ win | | document parse | `records-64k` | 64.2 KiB | 472 µs | unsupported | unsupported | unsupported | 6.38 ms (bytes-only) † | unsupported | unsupported | 514 µs | 0.07× of `sharpyaml-syntax` † (bytes-only) ▲ win | | document parse | `records-1m` | 1 MiB | 7.52 ms | unsupported | unsupported | unsupported | 121 ms (bytes-only) † | unsupported | unsupported | 7.38 ms | 0.06× of `sharpyaml-syntax` † (bytes-only) ▲ win | | document parse | `records-8m` | 8 MiB | 60.1 ms | unsupported | unsupported | unsupported | 1.13 s (bytes-only) † | unsupported | unsupported | 59.1 ms | 0.05× of `sharpyaml-syntax` † (bytes-only) ▲ win | | document parse | `compose-merge-2k` | 2.14 KiB | 14 µs | unsupported | unsupported | unsupported | 128 µs (bytes-only) † | unsupported | unsupported | 17.3 µs | 0.11× of `sharpyaml-syntax` † (bytes-only) ▲ win | | document parse | `compose-merge-64k` | 64.1 KiB | 449 µs | unsupported | unsupported | unsupported | 5.29 ms (bytes-only) † | unsupported | unsupported | 406 µs | 0.08× of `sharpyaml-syntax` † (bytes-only) ▲ win | | document parse | `compose-merge-1m` | 1 MiB | 6.32 ms | unsupported | unsupported | unsupported | 109 ms (bytes-only) † | unsupported | unsupported | 6.08 ms | 0.06× of `sharpyaml-syntax` † (bytes-only) ▲ win | | document parse | `k8s-multi-16k` | 16.4 KiB | 124 µs | unsupported | unsupported | unsupported | 1.33 ms (bytes-only) † | unsupported | unsupported | 125 µs | 0.09× of `sharpyaml-syntax` † (bytes-only) ▲ win | | document parse | `k8s-multi-1m` | 1 MiB | 8.08 ms | unsupported | unsupported | unsupported | 119 ms (bytes-only) † | unsupported | unsupported | 7.49 ms | 0.07× of `sharpyaml-syntax` † (bytes-only) ▲ win | | document parse | `openapi-100k` | 101 KiB | 544 µs | unsupported | unsupported | unsupported | 6.89 ms (bytes-only) † | unsupported | unsupported | 554 µs | 0.08× of `sharpyaml-syntax` † (bytes-only) ▲ win | | document parse | `openapi-8m` | 8 MiB | 48.5 ms | unsupported | unsupported | unsupported | 840 ms (bytes-only) † | unsupported | unsupported | 43.1 ms | 0.06× of `sharpyaml-syntax` † (bytes-only) ▲ win | | document parse | `helm-values-64k` | 64 KiB | 275 µs | unsupported | unsupported | unsupported | 3.48 ms (bytes-only) † | unsupported | unsupported | 277 µs | 0.08× of `sharpyaml-syntax` † (bytes-only) ▲ win | | document parse | `helm-values-1m` | 1 MiB | 4.3 ms | unsupported | unsupported | unsupported | 79.9 ms (bytes-only) † | unsupported | unsupported | 4.54 ms | 0.05× of `sharpyaml-syntax` † (bytes-only) ▲ win | | document parse | `deep-block` | 5.23 KiB | 11.5 µs | unsupported | unsupported | unsupported | rejects [7] | unsupported | unsupported | 11.3 µs | no rival | | document parse | `deep-flow` | 202 B | 6.17 µs | unsupported | unsupported | unsupported | rejects [7] | unsupported | unsupported | 6.6 µs | no rival | | document parse | `wide-map-64k` | 64 KiB | 452 µs | unsupported | unsupported | unsupported | 4.83 ms (bytes-only) † | unsupported | unsupported | 455 µs | 0.09× of `sharpyaml-syntax` † (bytes-only) ▲ win | | document parse | `wide-map-1m` | 1 MiB | 5.86 ms | unsupported | unsupported | unsupported | 101 ms (bytes-only) † | unsupported | unsupported | 5.9 ms | 0.06× of `sharpyaml-syntax` † (bytes-only) ▲ win | | document parse | `long-scalars-256k` | 261 KiB | 41.8 µs | unsupported | unsupported | unsupported | 8.75 ms (bytes-only) † | unsupported | unsupported | 39.3 µs | <0.01× of `sharpyaml-syntax` † (bytes-only) ▲ win | | document parse | `flow-heavy-64k` | 64 KiB | 874 µs | unsupported | unsupported | unsupported | 10.3 ms (bytes-only) † | unsupported | unsupported | 874 µs | 0.08× of `sharpyaml-syntax` † (bytes-only) ▲ win | | document parse | `anchors-32k` | 32 KiB | 320 µs | unsupported | unsupported | unsupported | 4.1 ms (bytes-only) † | unsupported | unsupported | 322 µs | 0.08× of `sharpyaml-syntax` † (bytes-only) ▲ win | | document parse | `unicode-32k` | 32 KiB | 96.3 µs | unsupported | unsupported | unsupported | 1.65 ms (bytes-only) † | unsupported | unsupported | 101 µs | 0.06× of `sharpyaml-syntax` † (bytes-only) ▲ win | | document parse | `scalar-styles-32k` | 32 KiB | 159 µs | unsupported | unsupported | unsupported | 2.15 ms (bytes-only) † | unsupported | unsupported | 209 µs | 0.07× of `sharpyaml-syntax` † (bytes-only) ▲ win | | unchanged emit | `compose-wordpress` | 815 B | 55.7 ns | unsupported | unsupported | unsupported | 222 ns | unsupported | unsupported | 64.7 ns | 0.25× of `sharpyaml-syntax` ▲ win | | unchanged emit | `gha-workflow` | 9.43 KiB | 1.15 µs | unsupported | unsupported | unsupported | 2.1 µs | unsupported | unsupported | 868 ns | 0.55× of `sharpyaml-syntax` ▲ win | | unchanged emit | `helm-ingress-nginx` | 52.4 KiB | 2.59 µs | unsupported | unsupported | unsupported | 7.12 µs | unsupported | unsupported | 1.58 µs | 0.36× of `sharpyaml-syntax` ▲ win | | unchanged emit | `k8s-ingress-nginx-multi` | 16 KiB | 1.33 µs | unsupported | unsupported | unsupported | 2.45 µs | unsupported | unsupported | 1.24 µs | 0.54× of `sharpyaml-syntax` ▲ win | | unchanged emit | `openapi-api-with-examples` | 6.75 KiB | 997 ns | unsupported | unsupported | unsupported | 1.55 µs | unsupported | unsupported | 1.06 µs | 0.64× of `sharpyaml-syntax` ▲ win | | unchanged emit | `openapi-petstore` | 5.34 KiB | 626 ns | unsupported | unsupported | unsupported | 1.38 µs | unsupported | unsupported | 622 ns | 0.45× of `sharpyaml-syntax` ▲ win | | unchanged emit | `prometheus-config` | 1.1 KiB | 59.8 ns | unsupported | unsupported | unsupported | 275 ns | unsupported | unsupported | 85.2 ns | 0.22× of `sharpyaml-syntax` ▲ win | | unchanged emit | `reference-a` | 51 B | 11.6 ns | unsupported | unsupported | unsupported | 17.5 ns | unsupported | unsupported | 11.5 ns | 0.66× of `sharpyaml-syntax` ▲ win | | unchanged emit | `reference-b` | 51 B | 11.2 ns | unsupported | unsupported | unsupported | 15 ns | unsupported | unsupported | 16.6 ns | 0.75× of `sharpyaml-syntax` ▲ win | | unchanged emit | `records-4k` | 4.14 KiB | 540 ns | unsupported | unsupported | unsupported | 2.53 µs | unsupported | unsupported | 567 ns | 0.21× of `sharpyaml-syntax` ▲ win | | unchanged emit | `records-64k` | 64.2 KiB | 4.12 µs | unsupported | unsupported | unsupported | 9.38 µs | unsupported | unsupported | 3.73 µs | 0.44× of `sharpyaml-syntax` ▲ win | | unchanged emit | `records-1m` | 1 MiB | 49 µs | unsupported | unsupported | unsupported | 206 µs | unsupported | unsupported | 51 µs | 0.24× of `sharpyaml-syntax` ▲ win | | unchanged emit | `records-8m` | 8 MiB | 745 µs | unsupported | unsupported | unsupported | 4.2 ms | unsupported | unsupported | 660 µs | 0.18× of `sharpyaml-syntax` ▲ win | | unchanged emit | `compose-merge-2k` | 2.14 KiB | 419 ns | unsupported | unsupported | unsupported | 869 ns | unsupported | unsupported | 286 ns | 0.48× of `sharpyaml-syntax` ▲ win | | unchanged emit | `compose-merge-64k` | 64.1 KiB | 4.75 µs | unsupported | unsupported | unsupported | 8.46 µs | unsupported | unsupported | 5.46 µs | 0.56× of `sharpyaml-syntax` ▲ win | | unchanged emit | `compose-merge-1m` | 1 MiB | 82.2 µs | unsupported | unsupported | unsupported | 191 µs | unsupported | unsupported | 82.7 µs | 0.43× of `sharpyaml-syntax` ▲ win | | unchanged emit | `k8s-multi-16k` | 16.4 KiB | 1.27 µs | unsupported | unsupported | unsupported | 2.44 µs | unsupported | unsupported | 1.21 µs | 0.52× of `sharpyaml-syntax` ▲ win | | unchanged emit | `k8s-multi-1m` | 1 MiB | 51.8 µs | unsupported | unsupported | unsupported | 220 µs | unsupported | unsupported | 70 µs | 0.24× of `sharpyaml-syntax` ▲ win | | unchanged emit | `openapi-100k` | 101 KiB | 26.3 µs | unsupported | unsupported | unsupported | 13.3 µs | unsupported | unsupported | 23 µs | 1.97× of `sharpyaml-syntax` ▼ loss | | unchanged emit | `openapi-8m` | 8 MiB | 3.14 ms | unsupported | unsupported | unsupported | 4.09 ms | unsupported | unsupported | 3.17 ms | 0.77× of `sharpyaml-syntax` ▲ win | | unchanged emit | `helm-values-64k` | 64 KiB | 2.74 µs | unsupported | unsupported | unsupported | 8.94 µs | unsupported | unsupported | 3.81 µs | 0.31× of `sharpyaml-syntax` ▲ win | | unchanged emit | `helm-values-1m` | 1 MiB | 40.4 µs | unsupported | unsupported | unsupported | 181 µs | unsupported | unsupported | 39.6 µs | 0.22× of `sharpyaml-syntax` ▲ win | | unchanged emit | `deep-block` | 5.23 KiB | 1.35 µs | unsupported | unsupported | unsupported | rejects [7] | unsupported | unsupported | 454 ns | no rival | | unchanged emit | `deep-flow` | 202 B | 31.2 ns | unsupported | unsupported | unsupported | rejects [7] | unsupported | unsupported | 28.9 ns | no rival | | unchanged emit | `wide-map-64k` | 64 KiB | 3.63 µs | unsupported | unsupported | unsupported | 8.96 µs | unsupported | unsupported | 4.41 µs | 0.40× of `sharpyaml-syntax` ▲ win | | unchanged emit | `wide-map-1m` | 1 MiB | 45.4 µs | unsupported | unsupported | unsupported | 201 µs | unsupported | unsupported | 40.2 µs | 0.23× of `sharpyaml-syntax` ▲ win | | unchanged emit | `long-scalars-256k` | 261 KiB | 36.8 µs | unsupported | unsupported | unsupported | 32 µs | unsupported | unsupported | 36.1 µs | 1.15× of `sharpyaml-syntax` ▼ loss | | unchanged emit | `flow-heavy-64k` | 64 KiB | 5.31 µs | unsupported | unsupported | unsupported | 10.3 µs | unsupported | unsupported | 5.2 µs | 0.52× of `sharpyaml-syntax` ▲ win | | unchanged emit | `anchors-32k` | 32 KiB | 5.4 µs | unsupported | unsupported | unsupported | 5.37 µs | unsupported | unsupported | 5.27 µs | 1.01× of `sharpyaml-syntax` ● tie | | unchanged emit | `unicode-32k` | 32 KiB | 4.2 µs | unsupported | unsupported | unsupported | 12.1 µs | unsupported | unsupported | 4.62 µs | 0.35× of `sharpyaml-syntax` ▲ win | | unchanged emit | `scalar-styles-32k` | 32 KiB | 1.17 µs | unsupported | unsupported | unsupported | 4.71 µs | unsupported | unsupported | 1.28 µs | 0.25× of `sharpyaml-syntax` ▲ win | | edit script | `compose-wordpress` | 815 B | 5.15 µs | unsupported | unsupported | unsupported | 116 µs (harness-rendered) † | unsupported | unsupported | 5.7 µs | 0.04× of `sharpyaml-syntax` † (harness-rendered) ▲ win | | edit script | `gha-workflow` | 9.43 KiB | 8.42 µs | unsupported | unsupported | unsupported | 2.76 ms (harness-rendered) † | unsupported | unsupported | 8.6 µs | <0.01× of `sharpyaml-syntax` † (harness-rendered) ▲ win | | edit script | `helm-ingress-nginx` | 52.4 KiB | 27.9 µs | unsupported | unsupported | unsupported | 5.78 ms (harness-rendered) † | unsupported | unsupported | 28.2 µs | <0.01× of `sharpyaml-syntax` † (harness-rendered) ▲ win | | edit script | `openapi-api-with-examples` | 6.75 KiB | 7.16 µs | unsupported | unsupported | unsupported | 350 µs (harness-rendered) † | unsupported | unsupported | 6.55 µs | 0.02× of `sharpyaml-syntax` † (harness-rendered) ▲ win | | edit script | `openapi-petstore` | 5.34 KiB | 7.92 µs | unsupported | unsupported | unsupported | 778 µs (harness-rendered) † | unsupported | unsupported | 7.28 µs | 0.01× of `sharpyaml-syntax` † (harness-rendered) ▲ win | | edit script | `prometheus-config` | 1.1 KiB | 2.57 µs | unsupported | unsupported | unsupported | 57.5 µs (harness-rendered) † | unsupported | unsupported | 3.32 µs | 0.04× of `sharpyaml-syntax` † (harness-rendered) ▲ win | | edit script | `reference-a` | 51 B | 1.44 µs | unsupported | unsupported | unsupported | 5.22 µs (harness-rendered) † | unsupported | unsupported | 1.45 µs | 0.28× of `sharpyaml-syntax` † (harness-rendered) ▲ win | | edit script | `reference-b` | 51 B | 1.58 µs | unsupported | unsupported | unsupported | 5.31 µs (harness-rendered) † | unsupported | unsupported | 1.38 µs | 0.30× of `sharpyaml-syntax` † (harness-rendered) ▲ win | | edit script | `records-4k` | 4.14 KiB | 8.6 µs | unsupported | unsupported | unsupported | 1.22 ms (harness-rendered) † | unsupported | unsupported | 8.96 µs | <0.01× of `sharpyaml-syntax` † (harness-rendered) ▲ win | | edit script | `records-64k` | 64.2 KiB | 45.4 µs | unsupported | unsupported | unsupported | 33.1 ms (harness-rendered) † | unsupported | unsupported | 42.2 µs | <0.01× of `sharpyaml-syntax` † (harness-rendered) ▲ win | | edit script | `records-1m` | 1 MiB | 740 µs | unsupported | unsupported | unsupported | 624 ms (harness-rendered) † | unsupported | unsupported | 672 µs | <0.01× of `sharpyaml-syntax` † (harness-rendered) ▲ win | | edit script | `records-8m` | 8 MiB | 11.6 ms | unsupported | unsupported | unsupported | 4.55 s (harness-rendered) † | unsupported | unsupported | 11.7 ms | <0.01× of `sharpyaml-syntax` † (harness-rendered) ▲ win | | edit script | `compose-merge-2k` | 2.14 KiB | 8.52 µs | unsupported | unsupported | unsupported | 389 µs (harness-rendered) † | unsupported | unsupported | 8.54 µs | 0.02× of `sharpyaml-syntax` † (harness-rendered) ▲ win | | edit script | `compose-merge-64k` | 64.1 KiB | 77 µs | unsupported | unsupported | unsupported | 22.1 ms (harness-rendered) † | unsupported | unsupported | 79.1 µs | <0.01× of `sharpyaml-syntax` † (harness-rendered) ▲ win | | edit script | `compose-merge-1m` | 1 MiB | 1.33 ms | unsupported | unsupported | unsupported | 412 ms (harness-rendered) † | unsupported | unsupported | 1.23 ms | <0.01× of `sharpyaml-syntax` † (harness-rendered) ▲ win | | edit script | `openapi-100k` | 101 KiB | 44.5 µs | unsupported | unsupported | unsupported | 31.8 ms (harness-rendered) † | unsupported | unsupported | 50.1 µs | <0.01× of `sharpyaml-syntax` † (harness-rendered) ▲ win | | edit script | `openapi-8m` | 8 MiB | 9.51 ms | unsupported | unsupported | unsupported | 2.86 s (harness-rendered) † | unsupported | unsupported | 9.46 ms | <0.01× of `sharpyaml-syntax` † (harness-rendered) ▲ win | | edit script | `helm-values-64k` | 64 KiB | 56.8 µs | unsupported | unsupported | unsupported | 16.6 ms (harness-rendered) † | unsupported | unsupported | 54 µs | <0.01× of `sharpyaml-syntax` † (harness-rendered) ▲ win | | edit script | `helm-values-1m` | 1 MiB | 770 µs | unsupported | unsupported | unsupported | 303 ms (harness-rendered) † | unsupported | unsupported | 756 µs | <0.01× of `sharpyaml-syntax` † (harness-rendered) ▲ win | | edit script | `wide-map-64k` | 64 KiB | 125 µs | unsupported | unsupported | unsupported | 16 ms (harness-rendered) † | unsupported | unsupported | 116 µs | <0.01× of `sharpyaml-syntax` † (harness-rendered) ▲ win | | edit script | `wide-map-1m` | 1 MiB | 1.86 ms | unsupported | unsupported | unsupported | 254 ms (harness-rendered) † | unsupported | unsupported | 1.84 ms | <0.01× of `sharpyaml-syntax` † (harness-rendered) ▲ win | | patch (RFC 6902) | `compose-wordpress` | 815 B | 4.52 µs | unsupported | unsupported | unsupported | unsupported | unsupported | differs [12] | 4.73 µs | no rival | | patch (RFC 6902) | `gha-workflow` | 9.43 KiB | 8.51 µs | unsupported | unsupported | unsupported | unsupported | unsupported | differs [12] | 8.37 µs | no rival | | patch (RFC 6902) | `reference-a` | 51 B | 1.51 µs | unsupported | unsupported | unsupported | unsupported | unsupported | differs [12] | 1.58 µs | no rival | | patch (RFC 6902) | `reference-b` | 51 B | 1.45 µs | unsupported | unsupported | unsupported | unsupported | unsupported | differs [12] | 1.42 µs | no rival | | patch (RFC 6902) | `records-4k` | 4.14 KiB | 7.5 µs | unsupported | unsupported | unsupported | unsupported | unsupported | differs [12] | 7.45 µs | no rival | | patch (RFC 6902) | `records-64k` | 64.2 KiB | 59.2 µs | unsupported | unsupported | unsupported | unsupported | unsupported | differs [12] | 52.1 µs | no rival | | patch (RFC 6902) | `records-1m` | 1 MiB | 988 µs | unsupported | unsupported | unsupported | unsupported | unsupported | differs [12] | 1.02 ms | no rival | | patch (RFC 6902) | `records-8m` | 8 MiB | 14.8 ms | unsupported | unsupported | unsupported | unsupported | unsupported | differs [12] | 14.4 ms | no rival | **Allocated bytes per task (MemoryDiagnoser)** | Task | Workload | Input | `oneyaml-csharp` | `yamldotnet` | `sharpyaml` | `sharpyaml-model` | `sharpyaml-syntax` | `vyaml` | `yaml2jsonnode` | `oneyaml-csharp-gen` | Ours vs strongest competitor | |---|---|---|---|---|---|---|---|---|---|---|---| | typed load | `gha-workflow` | 9.43 KiB | 27.5 KiB | 397 KiB | 204 KiB | unsupported | unsupported | 30.6 KiB | 395 KiB | 27.5 KiB | 0.90× of `vyaml` ▲ win | | typed load | `records-4k` | 4.14 KiB | 11.2 KiB | 349 KiB | 156 KiB | unsupported | unsupported | 11.2 KiB | 325 KiB | 11.2 KiB | 1.00× of `vyaml` ● tie | | typed load | `records-64k` | 64.2 KiB | 153 KiB | 5.14 MiB | 2.28 MiB | unsupported | unsupported | 171 KiB | 4.77 MiB | 153 KiB | 0.90× of `vyaml` ▲ win | | typed load | `records-1m` | 1 MiB | 2.34 MiB | 80.7 MiB | 35.8 MiB | unsupported | unsupported | 2.64 MiB | 74.9 MiB | 2.34 MiB | 0.89× of `vyaml` ▲ win | | typed load | `records-8m` | 8 MiB | 18.6 MiB | 660 MiB | 283 MiB | unsupported | unsupported | 21 MiB | 593 MiB | 18.6 MiB | 0.89× of `vyaml` ▲ win | | generic load | `compose-wordpress` | 815 B | 3.34 KiB | 42.4 KiB | 24 KiB | 20.9 KiB | unsupported | 3.6 KiB | 34.3 KiB | 3.34 KiB | 0.93× of `vyaml` ▲ win | | generic load | `gha-workflow` | 9.43 KiB | 39.3 KiB | 434 KiB | 237 KiB | 220 KiB | unsupported | 48.2 KiB | 357 KiB | 39.3 KiB | 0.82× of `vyaml` ▲ win | | generic load | `helm-ingress-nginx` | 52.4 KiB | 52.9 KiB | 690 KiB | 412 KiB | 392 KiB | unsupported | 67.4 KiB | 599 KiB | 52.9 KiB | 0.78× of `vyaml` ▲ win | | generic load | `k8s-ingress-nginx-multi` | 16 KiB | 69.9 KiB | 937 KiB | unsupported | 450 KiB | unsupported | 105 KiB | 758 KiB | 69.9 KiB | 0.67× of `vyaml` ▲ win | | generic load | `openapi-api-with-examples` | 6.75 KiB | 20.8 KiB | 211 KiB | 112 KiB | 105 KiB | unsupported | 21.6 KiB | 170 KiB | 20.8 KiB | 0.96× of `vyaml` ▲ win | | generic load | `openapi-petstore` | 5.34 KiB | 30.1 KiB | 255 KiB | 143 KiB | 132 KiB | unsupported | 31.5 KiB | 216 KiB | 30.1 KiB | 0.95× of `vyaml` ▲ win | | generic load | `prometheus-config` | 1.1 KiB | 3.15 KiB | 37.7 KiB | 20.7 KiB | 19.6 KiB | unsupported | 3.3 KiB | 32.1 KiB | 3.15 KiB | 0.95× of `vyaml` ▲ win | | generic load | `reference-a` | 51 B | 320 B | 12.5 KiB | 5.09 KiB | 4.97 KiB | unsupported | 392 B | 11.1 KiB | 320 B | 0.82× of `vyaml` ▲ win | | generic load | `reference-b` | 51 B | 320 B | 12.3 KiB | 5.09 KiB | 4.97 KiB | unsupported | 392 B | 11.1 KiB | 320 B | 0.82× of `vyaml` ▲ win | | generic load | `records-4k` | 4.14 KiB | 34.8 KiB | 378 KiB | 199 KiB | 182 KiB | unsupported | 42 KiB | 313 KiB | 34.8 KiB | 0.83× of `vyaml` ▲ win | | generic load | `records-64k` | 64.2 KiB | 414 KiB | 5.58 MiB | 2.93 MiB | 2.67 MiB | unsupported | 648 KiB | 4.61 MiB | 414 KiB | 0.64× of `vyaml` ▲ win | | generic load | `records-1m` | 1 MiB | 6.33 MiB | 87.6 MiB | 45.9 MiB | 42 MiB | unsupported | 9.96 MiB | 72.3 MiB | 6.33 MiB | 0.64× of `vyaml` ▲ win | | generic load | `records-8m` | 8 MiB | 50.2 MiB | 693 MiB | 364 MiB | 332 MiB | unsupported | 78.9 MiB | 572 MiB | 50.2 MiB | 0.64× of `vyaml` ▲ win | | generic load | `compose-merge-2k` | 2.14 KiB | 16.7 KiB | 179 KiB | 95.3 KiB | 110 KiB | unsupported | 26.6 KiB | 162 KiB | 16.7 KiB | 0.63× of `vyaml` ▲ win | | generic load | `compose-merge-64k` | 64.1 KiB | 338 KiB | 5.02 MiB | 2.52 MiB | 3.01 MiB | unsupported | 762 KiB | 4.37 MiB | 338 KiB | 0.44× of `vyaml` ▲ win | | generic load | `compose-merge-1m` | 1 MiB | 5.28 MiB | 74.4 MiB | 39.5 MiB | 47 MiB | unsupported | 11.7 MiB | 68.1 MiB | 5.28 MiB | 0.45× of `vyaml` ▲ win | | generic load | `k8s-multi-16k` | 16.4 KiB | 138 KiB | 1.61 MiB | unsupported | 804 KiB | unsupported | 184 KiB | 1.33 MiB | 138 KiB | 0.75× of `vyaml` ▲ win | | generic load | `k8s-multi-1m` | 1 MiB | 8.15 MiB | 98.4 MiB | unsupported | 48 MiB | unsupported | 11 MiB | 81 MiB | 8.15 MiB | 0.74× of `vyaml` ▲ win | | generic load | `openapi-100k` | 101 KiB | 611 KiB | 7.02 MiB | 3.72 MiB | 3.41 MiB | unsupported | 829 KiB | 5.81 MiB | 611 KiB | 0.74× of `vyaml` ▲ win | | generic load | `openapi-8m` | 8 MiB | 48.2 MiB | 564 MiB | 299 MiB | 275 MiB | unsupported | 65.4 MiB | 468 MiB | 48.2 MiB | 0.74× of `vyaml` ▲ win | | generic load | `helm-values-64k` | 64 KiB | 213 KiB | 3.27 MiB | 1.67 MiB | 1.59 MiB | unsupported | 337 KiB | 2.64 MiB | 213 KiB | 0.63× of `vyaml` ▲ win | | generic load | `helm-values-1m` | 1 MiB | 3.34 MiB | 54.5 MiB | 26.6 MiB | 25.2 MiB | unsupported | 5.28 MiB | 41.9 MiB | 3.34 MiB | 0.63× of `vyaml` ▲ win | | generic load | `deep-block` | 5.23 KiB | 21.3 KiB | 200 KiB | rejects [1] | rejects [6] | unsupported | 23.7 KiB | 171 KiB | 21.3 KiB | 0.90× of `vyaml` ▲ win | | generic load | `deep-flow` | 202 B | 8.62 KiB | 130 KiB | rejects [1] | rejects [6] | unsupported | 8.62 KiB | 91.5 KiB | 8.62 KiB | 1.00× of `vyaml` ● tie | | generic load | `wide-map-64k` | 64 KiB | 448 KiB | 4.94 MiB | 2.31 MiB | 2.26 MiB | unsupported | 462 KiB | 3.77 MiB | 448 KiB | 0.97× of `vyaml` ▲ win | | generic load | `wide-map-1m` | 1 MiB | 7.25 MiB | 68.5 MiB | 35.7 MiB | 33.9 MiB | unsupported | 7.45 MiB | 56.8 MiB | 7.25 MiB | 0.97× of `vyaml` ▲ win | | generic load | `long-scalars-256k` | 261 KiB | 516 KiB | 1.58 MiB | 1.88 MiB | 1.88 MiB | unsupported | 515 KiB | 1.58 MiB | 516 KiB | 1.00× of `vyaml` ● tie | | generic load | `flow-heavy-64k` | 64 KiB | 646 KiB | 10.6 MiB | 5 MiB | 4.72 MiB | unsupported | 1.02e+03 KiB | 8.48 MiB | 646 KiB | 0.63× of `vyaml` ▲ win | | generic load | `anchors-32k` | 32 KiB | 312 KiB | 2.89 MiB | 1.78 MiB | 4.06 MiB | unsupported | 1.13 MiB | 4.32 MiB | 312 KiB | 0.27× of `vyaml` ▲ win | | generic load | `unicode-32k` | 32 KiB | 103 KiB | 1.07 MiB | 522 KiB | 567 KiB | unsupported | 102 KiB | 886 KiB | 103 KiB | 1.01× of `vyaml` ● tie | | generic load | `scalar-styles-32k` | 32 KiB | 115 KiB | 1.63 MiB | 1.05 MiB | 1.04 MiB | unsupported | 178 KiB | 1.34 MiB | 115 KiB | 0.65× of `vyaml` ▲ win | | dump | `compose-wordpress` | 815 B | 720 B | 37.9 KiB | 6.7 KiB | 8.54 KiB | unsupported | 65.1 KiB | 117 KiB | 720 B | 0.10× of `sharpyaml` ▲ win | | dump | `gha-workflow` | 9.43 KiB | 8.41 KiB | 256 KiB | 35.1 KiB | differs [5] | unsupported | 78.4 KiB | 350 KiB | 8.41 KiB | 0.24× of `sharpyaml` ▲ win | | dump | `helm-ingress-nginx` | 52.4 KiB | 9.13 KiB | 374 KiB | 29.5 KiB | 80.3 KiB | unsupported | differs [11] | 450 KiB | 9.13 KiB | 0.31× of `sharpyaml` ▲ win | | dump | `openapi-api-with-examples` | 6.75 KiB | 4.2 KiB | 132 KiB | 14.9 KiB | 28.6 KiB | unsupported | 70.9 KiB | 217 KiB | 4.2 KiB | 0.28× of `sharpyaml` ▲ win | | dump | `openapi-petstore` | 5.34 KiB | 5.41 KiB | 161 KiB | 18.1 KiB | 46.1 KiB | unsupported | 73.2 KiB | 251 KiB | 5.41 KiB | 0.30× of `sharpyaml` ▲ win | | dump | `prometheus-config` | 1.1 KiB | 408 B | 34.4 KiB | 2.59 KiB | 5.48 KiB | unsupported | 65.3 KiB | 115 KiB | 408 B | 0.15× of `sharpyaml` ▲ win | | dump | `reference-a` | 51 B | 112 B | 18.5 KiB | 1.55 KiB | 1.93 KiB | unsupported | differs [9] | 101 KiB | 112 B | 0.07× of `sharpyaml` ▲ win | | dump | `reference-b` | 51 B | 112 B | 18.5 KiB | 1.55 KiB | 1.93 KiB | unsupported | differs [9] | 101 KiB | 112 B | 0.07× of `sharpyaml` ▲ win | | dump | `records-4k` | 4.14 KiB | 4.23 KiB | differs [14] | differs [2] | 49.6 KiB | unsupported | differs [10] | 298 KiB | 4.23 KiB | 0.09× of `sharpyaml-model` ▲ win | | dump | `records-64k` | 64.2 KiB | 64.8 KiB | differs [14] | differs [2] | 636 KiB | unsupported | differs [10] | 2.98 MiB | 64.8 KiB | 0.10× of `sharpyaml-model` ▲ win | | dump | `records-1m` | 1 MiB | 1.01 MiB | differs [14] | differs [2] | 9.7 MiB | unsupported | differs [10] | 45.4 MiB | 1.01 MiB | 0.10× of `sharpyaml-model` ▲ win | | dump | `records-8m` | 8 MiB | 8.05 MiB | differs [14] | differs [2] | 77.4 MiB | unsupported | differs [10] | 359 MiB | 8.05 MiB | 0.10× of `sharpyaml-model` ▲ win | | dump | `compose-merge-2k` | 2.14 KiB | 4.11 KiB | 123 KiB | 13 KiB | 39.8 KiB | unsupported | 75.2 KiB | 273 KiB | 4.11 KiB | 0.32× of `sharpyaml` ▲ win | | dump | `compose-merge-64k` | 64.1 KiB | 124 KiB | 2.91 MiB | 350 KiB | 904 KiB | unsupported | 519 KiB | 4.94 MiB | 124 KiB | 0.35× of `sharpyaml` ▲ win | | dump | `compose-merge-1m` | 1 MiB | 1.92 MiB | 45.3 MiB | 8.9 MiB | 13.8 MiB | unsupported | 8.91 MiB | 75.6 MiB | 1.92 MiB | 0.22× of `sharpyaml` ▲ win | | dump | `openapi-100k` | 101 KiB | 104 KiB | 3.88 MiB | 312 KiB | 849 KiB | unsupported | 464 KiB | 4.12 MiB | 104 KiB | 0.33× of `sharpyaml` ▲ win | | dump | `openapi-8m` | 8 MiB | 8.25 MiB | 311 MiB | 41.2 MiB | 62.7 MiB | unsupported | 53.2 MiB | 324 MiB | 8.25 MiB | 0.20× of `sharpyaml` ▲ win | | dump | `helm-values-64k` | 64 KiB | 39.4 KiB | 1.73 MiB | 120 KiB | 365 KiB | unsupported | 187 KiB | 1.83 MiB | 39.4 KiB | 0.33× of `sharpyaml` ▲ win | | dump | `helm-values-1m` | 1 MiB | 631 KiB | 26.3 MiB | 1.85 MiB | 5.43 MiB | unsupported | 3.84 MiB | 27.4 MiB | 631 KiB | 0.33× of `sharpyaml` ▲ win | | dump | `deep-block` | 5.23 KiB | 10.2 KiB | rejects [13] | rejects [1] | rejects [6] | unsupported | 72.9 KiB | 277 KiB | 10.2 KiB | 0.14× of `vyaml` ▲ win | | dump | `deep-flow` | 202 B | 280 B | rejects [13] | rejects [1] | rejects [6] | unsupported | 68.1 KiB | 145 KiB | 280 B | <0.01× of `vyaml` ▲ win | | dump | `wide-map-64k` | 64 KiB | 64.2 KiB | 2.37 MiB | 236 KiB | 464 KiB | unsupported | 309 KiB | 2.34 MiB | 64.2 KiB | 0.27× of `sharpyaml` ▲ win | | dump | `wide-map-1m` | 1 MiB | 1 MiB | 34.5 MiB | 5.64 MiB | 7.03 MiB | unsupported | 5.59 MiB | 34.2 MiB | 1 MiB | 0.18× of `vyaml` ▲ win | | dump | `long-scalars-256k` | 261 KiB | 260 KiB | 1.55 MiB | 776 KiB | differs [4] | unsupported | differs [8] | 1.38 MiB | 260 KiB | 0.33× of `sharpyaml` ▲ win | | dump | `flow-heavy-64k` | 64 KiB | 84.4 KiB | 6.23 MiB | 333 KiB | 848 KiB | unsupported | 472 KiB | 5.03 MiB | 84.4 KiB | 0.25× of `sharpyaml` ▲ win | | dump | `anchors-32k` | 32 KiB | 145 KiB | 1.55 MiB | 447 KiB | 1.42 MiB | unsupported | 863 KiB | 9.8 MiB | 145 KiB | 0.33× of `sharpyaml` ▲ win | | dump | `unicode-32k` | 32 KiB | 25.9 KiB | 606 KiB | 68.3 KiB | differs [3] | unsupported | 64.2 KiB | 578 KiB | 25.9 KiB | 0.40× of `vyaml` ▲ win | | dump | `scalar-styles-32k` | 32 KiB | 29.7 KiB | 923 KiB | 87.5 KiB | 230 KiB | unsupported | differs [9] | 927 KiB | 29.7 KiB | 0.34× of `sharpyaml` ▲ win | | document parse | `compose-wordpress` | 815 B | 264 B | unsupported | unsupported | unsupported | 52 KiB † | unsupported | unsupported | 264 B | <0.01× of `sharpyaml-syntax` † (bytes-only) ▲ win | | document parse | `gha-workflow` | 9.43 KiB | 264 B | unsupported | unsupported | unsupported | 647 KiB † | unsupported | unsupported | 264 B | <0.01× of `sharpyaml-syntax` † (bytes-only) ▲ win | | document parse | `helm-ingress-nginx` | 52.4 KiB | 264 B | unsupported | unsupported | unsupported | 1.26 MiB † | unsupported | unsupported | 264 B | <0.01× of `sharpyaml-syntax` † (bytes-only) ▲ win | | document parse | `k8s-ingress-nginx-multi` | 16 KiB | 264 B | unsupported | unsupported | unsupported | 1.24 MiB † | unsupported | unsupported | 264 B | <0.01× of `sharpyaml-syntax` † (bytes-only) ▲ win | | document parse | `openapi-api-with-examples` | 6.75 KiB | 264 B | unsupported | unsupported | unsupported | 231 KiB † | unsupported | unsupported | 264 B | <0.01× of `sharpyaml-syntax` † (bytes-only) ▲ win | | document parse | `openapi-petstore` | 5.34 KiB | 264 B | unsupported | unsupported | unsupported | 349 KiB † | unsupported | unsupported | 264 B | <0.01× of `sharpyaml-syntax` † (bytes-only) ▲ win | | document parse | `prometheus-config` | 1.1 KiB | 264 B | unsupported | unsupported | unsupported | 48.4 KiB † | unsupported | unsupported | 264 B | <0.01× of `sharpyaml-syntax` † (bytes-only) ▲ win | | document parse | `reference-a` | 51 B | 264 B | unsupported | unsupported | unsupported | 9.82 KiB † | unsupported | unsupported | 264 B | 0.03× of `sharpyaml-syntax` † (bytes-only) ▲ win | | document parse | `reference-b` | 51 B | 264 B | unsupported | unsupported | unsupported | 9.82 KiB † | unsupported | unsupported | 264 B | 0.03× of `sharpyaml-syntax` † (bytes-only) ▲ win | | document parse | `records-4k` | 4.14 KiB | 264 B | unsupported | unsupported | unsupported | 416 KiB † | unsupported | unsupported | 264 B | <0.01× of `sharpyaml-syntax` † (bytes-only) ▲ win | | document parse | `records-64k` | 64.2 KiB | 264 B | unsupported | unsupported | unsupported | 6.24 MiB † | unsupported | unsupported | 264 B | <0.01× of `sharpyaml-syntax` † (bytes-only) ▲ win | | document parse | `records-1m` | 1 MiB | 264 B | unsupported | unsupported | unsupported | 98.6 MiB † | unsupported | unsupported | 264 B | <0.01× of `sharpyaml-syntax` † (bytes-only) ▲ win | | document parse | `records-8m` | 8 MiB | 264 B | unsupported | unsupported | unsupported | 784 MiB † | unsupported | unsupported | 264 B | <0.01× of `sharpyaml-syntax` † (bytes-only) ▲ win | | document parse | `compose-merge-2k` | 2.14 KiB | 264 B | unsupported | unsupported | unsupported | 206 KiB † | unsupported | unsupported | 264 B | <0.01× of `sharpyaml-syntax` † (bytes-only) ▲ win | | document parse | `compose-merge-64k` | 64.1 KiB | 264 B | unsupported | unsupported | unsupported | 5.85 MiB † | unsupported | unsupported | 264 B | <0.01× of `sharpyaml-syntax` † (bytes-only) ▲ win | | document parse | `compose-merge-1m` | 1 MiB | 264 B | unsupported | unsupported | unsupported | 92.1 MiB † | unsupported | unsupported | 264 B | <0.01× of `sharpyaml-syntax` † (bytes-only) ▲ win | | document parse | `k8s-multi-16k` | 16.4 KiB | 264 B | unsupported | unsupported | unsupported | 1.62 MiB † | unsupported | unsupported | 264 B | <0.01× of `sharpyaml-syntax` † (bytes-only) ▲ win | | document parse | `k8s-multi-1m` | 1 MiB | 264 B | unsupported | unsupported | unsupported | 101 MiB † | unsupported | unsupported | 264 B | <0.01× of `sharpyaml-syntax` † (bytes-only) ▲ win | | document parse | `openapi-100k` | 101 KiB | 264 B | unsupported | unsupported | unsupported | 9.47 MiB † | unsupported | unsupported | 264 B | <0.01× of `sharpyaml-syntax` † (bytes-only) ▲ win | | document parse | `openapi-8m` | 8 MiB | 264 B | unsupported | unsupported | unsupported | 680 MiB † | unsupported | unsupported | 264 B | <0.01× of `sharpyaml-syntax` † (bytes-only) ▲ win | | document parse | `helm-values-64k` | 64 KiB | 264 B | unsupported | unsupported | unsupported | 4.8 MiB † | unsupported | unsupported | 264 B | <0.01× of `sharpyaml-syntax` † (bytes-only) ▲ win | | document parse | `helm-values-1m` | 1 MiB | 264 B | unsupported | unsupported | unsupported | 76.3 MiB † | unsupported | unsupported | 264 B | <0.01× of `sharpyaml-syntax` † (bytes-only) ▲ win | | document parse | `deep-block` | 5.23 KiB | 264 B | unsupported | unsupported | unsupported | rejects [7] | unsupported | unsupported | 264 B | no rival | | document parse | `deep-flow` | 202 B | 264 B | unsupported | unsupported | unsupported | rejects [7] | unsupported | unsupported | 264 B | no rival | | document parse | `wide-map-64k` | 64 KiB | 264 B | unsupported | unsupported | unsupported | 5.97 MiB † | unsupported | unsupported | 264 B | <0.01× of `sharpyaml-syntax` † (bytes-only) ▲ win | | document parse | `wide-map-1m` | 1 MiB | 264 B | unsupported | unsupported | unsupported | 90.5 MiB † | unsupported | unsupported | 264 B | <0.01× of `sharpyaml-syntax` † (bytes-only) ▲ win | | document parse | `long-scalars-256k` | 261 KiB | 264 B | unsupported | unsupported | unsupported | 9.98 MiB † | unsupported | unsupported | 264 B | <0.01× of `sharpyaml-syntax` † (bytes-only) ▲ win | | document parse | `flow-heavy-64k` | 64 KiB | 264 B | unsupported | unsupported | unsupported | 11.5 MiB † | unsupported | unsupported | 264 B | <0.01× of `sharpyaml-syntax` † (bytes-only) ▲ win | | document parse | `anchors-32k` | 32 KiB | 264 B | unsupported | unsupported | unsupported | 5.28 MiB † | unsupported | unsupported | 264 B | <0.01× of `sharpyaml-syntax` † (bytes-only) ▲ win | | document parse | `unicode-32k` | 32 KiB | 264 B | unsupported | unsupported | unsupported | 2.29 MiB † | unsupported | unsupported | 264 B | <0.01× of `sharpyaml-syntax` † (bytes-only) ▲ win | | document parse | `scalar-styles-32k` | 32 KiB | 264 B | unsupported | unsupported | unsupported | 3.04 MiB † | unsupported | unsupported | 264 B | <0.01× of `sharpyaml-syntax` † (bytes-only) ▲ win | | unchanged emit | `compose-wordpress` | 815 B | 840 B | unsupported | unsupported | unsupported | 840 B | unsupported | unsupported | 840 B | 1.00× of `sharpyaml-syntax` ● tie | | unchanged emit | `gha-workflow` | 9.43 KiB | 9.45 KiB | unsupported | unsupported | unsupported | 9.45 KiB | unsupported | unsupported | 9.45 KiB | 1.00× of `sharpyaml-syntax` ● tie | | unchanged emit | `helm-ingress-nginx` | 52.4 KiB | 52.4 KiB | unsupported | unsupported | unsupported | 52.4 KiB | unsupported | unsupported | 52.4 KiB | 1.00× of `sharpyaml-syntax` ● tie | | unchanged emit | `k8s-ingress-nginx-multi` | 16 KiB | 16 KiB | unsupported | unsupported | unsupported | 16 KiB | unsupported | unsupported | 16 KiB | 1.00× of `sharpyaml-syntax` ● tie | | unchanged emit | `openapi-api-with-examples` | 6.75 KiB | 6.77 KiB | unsupported | unsupported | unsupported | 6.77 KiB | unsupported | unsupported | 6.77 KiB | 1.00× of `sharpyaml-syntax` ● tie | | unchanged emit | `openapi-petstore` | 5.34 KiB | 5.37 KiB | unsupported | unsupported | unsupported | 5.37 KiB | unsupported | unsupported | 5.37 KiB | 1.00× of `sharpyaml-syntax` ● tie | | unchanged emit | `prometheus-config` | 1.1 KiB | 1.12 KiB | unsupported | unsupported | unsupported | 1.12 KiB | unsupported | unsupported | 1.12 KiB | 1.00× of `sharpyaml-syntax` ● tie | | unchanged emit | `reference-a` | 51 B | 80 B | unsupported | unsupported | unsupported | 80 B | unsupported | unsupported | 80 B | 1.00× of `sharpyaml-syntax` ● tie | | unchanged emit | `reference-b` | 51 B | 80 B | unsupported | unsupported | unsupported | 80 B | unsupported | unsupported | 80 B | 1.00× of `sharpyaml-syntax` ● tie | | unchanged emit | `records-4k` | 4.14 KiB | 4.17 KiB | unsupported | unsupported | unsupported | 4.17 KiB | unsupported | unsupported | 4.17 KiB | 1.00× of `sharpyaml-syntax` ● tie | | unchanged emit | `records-64k` | 64.2 KiB | 64.2 KiB | unsupported | unsupported | unsupported | 64.2 KiB | unsupported | unsupported | 64.2 KiB | 1.00× of `sharpyaml-syntax` ● tie | | unchanged emit | `records-1m` | 1 MiB | 1 MiB | unsupported | unsupported | unsupported | 1 MiB | unsupported | unsupported | 1 MiB | 1.00× of `sharpyaml-syntax` ● tie | | unchanged emit | `records-8m` | 8 MiB | 8 MiB | unsupported | unsupported | unsupported | 8 MiB | unsupported | unsupported | 8 MiB | 1.00× of `sharpyaml-syntax` ● tie | | unchanged emit | `compose-merge-2k` | 2.14 KiB | 2.16 KiB | unsupported | unsupported | unsupported | 2.16 KiB | unsupported | unsupported | 2.16 KiB | 1.00× of `sharpyaml-syntax` ● tie | | unchanged emit | `compose-merge-64k` | 64.1 KiB | 64.1 KiB | unsupported | unsupported | unsupported | 64.1 KiB | unsupported | unsupported | 64.1 KiB | 1.00× of `sharpyaml-syntax` ● tie | | unchanged emit | `compose-merge-1m` | 1 MiB | 1 MiB | unsupported | unsupported | unsupported | 1 MiB | unsupported | unsupported | 1 MiB | 1.00× of `sharpyaml-syntax` ● tie | | unchanged emit | `k8s-multi-16k` | 16.4 KiB | 16.4 KiB | unsupported | unsupported | unsupported | 16.4 KiB | unsupported | unsupported | 16.4 KiB | 1.00× of `sharpyaml-syntax` ● tie | | unchanged emit | `k8s-multi-1m` | 1 MiB | 1 MiB | unsupported | unsupported | unsupported | 1 MiB | unsupported | unsupported | 1 MiB | 1.00× of `sharpyaml-syntax` ● tie | | unchanged emit | `openapi-100k` | 101 KiB | 101 KiB | unsupported | unsupported | unsupported | 101 KiB | unsupported | unsupported | 101 KiB | 1.00× of `sharpyaml-syntax` ● tie | | unchanged emit | `openapi-8m` | 8 MiB | 8 MiB | unsupported | unsupported | unsupported | 8 MiB | unsupported | unsupported | 8 MiB | 1.00× of `sharpyaml-syntax` ● tie | | unchanged emit | `helm-values-64k` | 64 KiB | 64.1 KiB | unsupported | unsupported | unsupported | 64.1 KiB | unsupported | unsupported | 64.1 KiB | 1.00× of `sharpyaml-syntax` ● tie | | unchanged emit | `helm-values-1m` | 1 MiB | 1 MiB | unsupported | unsupported | unsupported | 1 MiB | unsupported | unsupported | 1 MiB | 1.00× of `sharpyaml-syntax` ● tie | | unchanged emit | `deep-block` | 5.23 KiB | 5.26 KiB | unsupported | unsupported | unsupported | rejects [7] | unsupported | unsupported | 5.26 KiB | no rival | | unchanged emit | `deep-flow` | 202 B | 232 B | unsupported | unsupported | unsupported | rejects [7] | unsupported | unsupported | 232 B | no rival | | unchanged emit | `wide-map-64k` | 64 KiB | 64 KiB | unsupported | unsupported | unsupported | 64 KiB | unsupported | unsupported | 64 KiB | 1.00× of `sharpyaml-syntax` ● tie | | unchanged emit | `wide-map-1m` | 1 MiB | 1 MiB | unsupported | unsupported | unsupported | 1 MiB | unsupported | unsupported | 1 MiB | 1.00× of `sharpyaml-syntax` ● tie | | unchanged emit | `long-scalars-256k` | 261 KiB | 261 KiB | unsupported | unsupported | unsupported | 261 KiB | unsupported | unsupported | 261 KiB | 1.00× of `sharpyaml-syntax` ● tie | | unchanged emit | `flow-heavy-64k` | 64 KiB | 64 KiB | unsupported | unsupported | unsupported | 64 KiB | unsupported | unsupported | 64 KiB | 1.00× of `sharpyaml-syntax` ● tie | | unchanged emit | `anchors-32k` | 32 KiB | 32 KiB | unsupported | unsupported | unsupported | 32 KiB | unsupported | unsupported | 32 KiB | 1.00× of `sharpyaml-syntax` ● tie | | unchanged emit | `unicode-32k` | 32 KiB | 32.1 KiB | unsupported | unsupported | unsupported | 32.1 KiB | unsupported | unsupported | 32.1 KiB | 1.00× of `sharpyaml-syntax` ● tie | | unchanged emit | `scalar-styles-32k` | 32 KiB | 32.1 KiB | unsupported | unsupported | unsupported | 32.1 KiB | unsupported | unsupported | 32.1 KiB | 1.00× of `sharpyaml-syntax` ● tie | | edit script | `compose-wordpress` | 815 B | 2.48 KiB | unsupported | unsupported | unsupported | 238 KiB † | unsupported | unsupported | 2.48 KiB | 0.01× of `sharpyaml-syntax` † (harness-rendered) ▲ win | | edit script | `gha-workflow` | 9.43 KiB | 11.1 KiB | unsupported | unsupported | unsupported | 2.93 MiB † | unsupported | unsupported | 11.1 KiB | <0.01× of `sharpyaml-syntax` † (harness-rendered) ▲ win | | edit script | `helm-ingress-nginx` | 52.4 KiB | 54.4 KiB | unsupported | unsupported | unsupported | 6.03 MiB † | unsupported | unsupported | 54.4 KiB | <0.01× of `sharpyaml-syntax` † (harness-rendered) ▲ win | | edit script | `openapi-api-with-examples` | 6.75 KiB | 7.7 KiB | unsupported | unsupported | unsupported | 621 KiB † | unsupported | unsupported | 7.7 KiB | 0.01× of `sharpyaml-syntax` † (harness-rendered) ▲ win | | edit script | `openapi-petstore` | 5.34 KiB | 5.5 KiB | unsupported | unsupported | unsupported | 1.48 MiB † | unsupported | unsupported | 5.5 KiB | <0.01× of `sharpyaml-syntax` † (harness-rendered) ▲ win | | edit script | `prometheus-config` | 1.1 KiB | 2.1 KiB | unsupported | unsupported | unsupported | 127 KiB † | unsupported | unsupported | 2.1 KiB | 0.02× of `sharpyaml-syntax` † (harness-rendered) ▲ win | | edit script | `reference-a` | 51 B | 536 B | unsupported | unsupported | unsupported | 15.6 KiB † | unsupported | unsupported | 536 B | 0.03× of `sharpyaml-syntax` † (harness-rendered) ▲ win | | edit script | `reference-b` | 51 B | 536 B | unsupported | unsupported | unsupported | 15.6 KiB † | unsupported | unsupported | 536 B | 0.03× of `sharpyaml-syntax` † (harness-rendered) ▲ win | | edit script | `records-4k` | 4.14 KiB | 5.7 KiB | unsupported | unsupported | unsupported | 1.92 MiB † | unsupported | unsupported | 5.7 KiB | <0.01× of `sharpyaml-syntax` † (harness-rendered) ▲ win | | edit script | `records-64k` | 64.2 KiB | 65.8 KiB | unsupported | unsupported | unsupported | 29.6 MiB † | unsupported | unsupported | 65.8 KiB | <0.01× of `sharpyaml-syntax` † (harness-rendered) ▲ win | | edit script | `records-1m` | 1 MiB | 1 MiB | unsupported | unsupported | unsupported | 469 MiB † | unsupported | unsupported | 1 MiB | <0.01× of `sharpyaml-syntax` † (harness-rendered) ▲ win | | edit script | `records-8m` | 8 MiB | 8 MiB | unsupported | unsupported | unsupported | 3.63 GiB † | unsupported | unsupported | 8 MiB | <0.01× of `sharpyaml-syntax` † (harness-rendered) ▲ win | | edit script | `compose-merge-2k` | 2.14 KiB | 3.42 KiB | unsupported | unsupported | unsupported | 752 KiB † | unsupported | unsupported | 3.42 KiB | <0.01× of `sharpyaml-syntax` † (harness-rendered) ▲ win | | edit script | `compose-merge-64k` | 64.1 KiB | 65.8 KiB | unsupported | unsupported | unsupported | 21.3 MiB † | unsupported | unsupported | 65.8 KiB | <0.01× of `sharpyaml-syntax` † (harness-rendered) ▲ win | | edit script | `compose-merge-1m` | 1 MiB | 1 MiB | unsupported | unsupported | unsupported | 335 MiB † | unsupported | unsupported | 1 MiB | <0.01× of `sharpyaml-syntax` † (harness-rendered) ▲ win | | edit script | `openapi-100k` | 101 KiB | 102 KiB | unsupported | unsupported | unsupported | 34 MiB † | unsupported | unsupported | 102 KiB | <0.01× of `sharpyaml-syntax` † (harness-rendered) ▲ win | | edit script | `openapi-8m` | 8 MiB | 8 MiB | unsupported | unsupported | unsupported | 2.43 GiB † | unsupported | unsupported | 8 MiB | <0.01× of `sharpyaml-syntax` † (harness-rendered) ▲ win | | edit script | `helm-values-64k` | 64 KiB | 65.6 KiB | unsupported | unsupported | unsupported | 17.2 MiB † | unsupported | unsupported | 65.6 KiB | <0.01× of `sharpyaml-syntax` † (harness-rendered) ▲ win | | edit script | `helm-values-1m` | 1 MiB | 1 MiB | unsupported | unsupported | unsupported | 274 MiB † | unsupported | unsupported | 1 MiB | <0.01× of `sharpyaml-syntax` † (harness-rendered) ▲ win | | edit script | `wide-map-64k` | 64 KiB | 64.9 KiB | unsupported | unsupported | unsupported | 15.7 MiB † | unsupported | unsupported | 64.9 KiB | <0.01× of `sharpyaml-syntax` † (harness-rendered) ▲ win | | edit script | `wide-map-1m` | 1 MiB | 1 MiB | unsupported | unsupported | unsupported | 237 MiB † | unsupported | unsupported | 1 MiB | <0.01× of `sharpyaml-syntax` † (harness-rendered) ▲ win | | patch (RFC 6902) | `compose-wordpress` | 815 B | 2.66 KiB | unsupported | unsupported | unsupported | unsupported | unsupported | differs [12] | 2.66 KiB | no rival | | patch (RFC 6902) | `gha-workflow` | 9.43 KiB | 10.8 KiB | unsupported | unsupported | unsupported | unsupported | unsupported | differs [12] | 10.8 KiB | no rival | | patch (RFC 6902) | `reference-a` | 51 B | 432 B | unsupported | unsupported | unsupported | unsupported | unsupported | differs [12] | 432 B | no rival | | patch (RFC 6902) | `reference-b` | 51 B | 432 B | unsupported | unsupported | unsupported | unsupported | unsupported | differs [12] | 432 B | no rival | | patch (RFC 6902) | `records-4k` | 4.14 KiB | 5.4 KiB | unsupported | unsupported | unsupported | unsupported | unsupported | differs [12] | 5.4 KiB | no rival | | patch (RFC 6902) | `records-64k` | 64.2 KiB | 65.5 KiB | unsupported | unsupported | unsupported | unsupported | unsupported | differs [12] | 65.5 KiB | no rival | | patch (RFC 6902) | `records-1m` | 1 MiB | 1 MiB | unsupported | unsupported | unsupported | unsupported | unsupported | differs [12] | 1 MiB | no rival | | patch (RFC 6902) | `records-8m` | 8 MiB | 8 MiB | unsupported | unsupported | unsupported | unsupported | unsupported | differs [12] | 8 MiB | no rival | Cells without a number, with what the correctness guard found: 1. `sharpyaml` (rejects; generic-load ×2, dump ×2): Stops at its default nesting limit, which the benchmark leaves as it is. What the guard found, in generic-load on `deep-block`: `default limit, not raised: YamlException: The maximum nesting depth of 64 has be…` 2. `sharpyaml` (differs; dump ×4): Writes a float with no fractional part (812.0) as 812, so a YAML 1.2 reader reads it back as an integer. What the guard found, in dump on `records-4k`: `read back: at /1/score: got 812, expected 812.0` 3. `sharpyaml-model` (differs; dump ×1): What the guard found, in dump on `unicode-32k`: `read back: at /2: got "Ø3DÞ00 grin 0", expected "\uD83D\uDE00 grin 0"` 4. `sharpyaml-model` (differs; dump ×1): What the guard found, in dump on `long-scalars-256k`: `read back: at /folded: got "epsilon zeta psi omega delta xi epsilon mu beta theta rho alpha psi delta delta…, expected "epsilon zeta psi omega delta xi epsilon mu beta theta rho alpha psi delta delta…` 5. `sharpyaml-model` (differs; dump ×1): What the guard found, in dump on `gha-workflow`: `read back: at /jobs/test-output/steps/2/run: got "echo \"Commit: ${{ steps.checkout.outputs.commit }}\"\necho \"Ref: ${{ steps.ch…, expected "echo \"Commit: ${{ steps.checkout.outputs.commit }}\"\nec…` 6. `sharpyaml-model` (rejects; generic-load ×2, dump ×2): Stops at its default nesting limit, which the benchmark leaves as it is. What the guard found, in generic-load on `deep-block`: `default limit, not raised: YamlException: The maximum nesting depth of 64 has be…` 7. `sharpyaml-syntax` (rejects; document-parse ×2, unchanged-emit ×2): Stops at its default nesting limit, which the benchmark leaves as it is. What the guard found, in document-parse on `deep-block`: `default limit, not raised: YamlException: The maximum nesting depth of 64 has be…` 8. `vyaml` (differs; dump ×1): What the guard found, in dump on `long-scalars-256k`: `read back: at /folded: got "epsilon zeta psi omega delta xi epsilon mu beta theta rho alpha psi delta delta…, expected "epsilon zeta psi omega delta xi epsilon mu beta theta rho alpha psi delta delta…` 9. `vyaml` (differs; dump ×3): A string that ends with a newline reads back without that final newline. What the guard found, in dump on `reference-a`: `read back: at /b: got "this is a test", expected "this is a test\n"` 10. `vyaml` (differs; dump ×4): Writes a float with no fractional part (812.0) as 812, so a YAML 1.2 reader reads it back as an integer. What the guard found, in dump on `records-4k`: `read back: at /1/score: got 812, expected 812.0` 11. `vyaml` (differs; dump ×1): Writes a string that looks like a number (05509, 1.5) without quotes, so it reads back as a number. What the guard found, in dump on `helm-ingress-nginx`: `read back: at /defaultBackend/image/tag: got 1.5, expected "1.5"` 12. `yaml2jsonnode` (differs; patch ×8): Patches a JSON tree and writes it back as YAML, so comments, blank lines and quoting are not kept (it is not lossless). What the guard found, in patch on `compose-wordpress`: `byte 20: got 694 bytes ":\n db:\n image: \"mariadb:11.4\"\n ", expected 860 bytes ":\n db:\n # We use a mariadb image"` 13. `yamldotnet` (rejects; dump ×2): Stops at its default nesting limit, which the benchmark leaves as it is. What the guard found, in dump on `deep-block`: `default limit, not raised: MaximumRecursionLevelReachedException: Too much recur…` 14. `yamldotnet` (differs; dump ×4): Writes a float with no fractional part (812.0) as 812, so a YAML 1.2 reader reads it back as an integer. What the guard found, in dump on `records-4k`: `read back: at /1/score: got 812, expected 812.0` Relative figures are ours ÷ the strongest competitor in that row, so below 1.0× is better for us and ▼ marks a loss. A † marks a cell measured differently from the task as specified; the note beside each mark says how. Cells without a number say why. The verdict of a task in the chart and the first table is never better than the real-world workloads allow, nor better than all of the task's workloads in these tables allow, generated and reference ones included, while its speed figure is over the real-world workloads. Method: **full run** of 2026-10-18T00:48:41Z. Kit 0.1.0 (revision 6cc83d12fbc6); text generated by kit revision c1647a46aa6d. `oneyaml-csharp c23a19250217`; competitors `yamldotnet 18.1.0`, `sharpyaml 3.16.0`, `sharpyaml-model 3.16.0`, `sharpyaml-syntax 3.16.0`, `vyaml 1.4.1`, `yaml2jsonnode 3.0.1`, `oneyaml-csharp-gen c23a19250217`. Runtime .NET 10.0.11 (target linux-x64), measured with BenchmarkDotNet 0.15.8: mean time per task from BenchmarkDotNet's ShortRun job (3 warm-up and 3 measured iterations) run in process; one invocation per iteration runs a batch of prepared tasks and the time is divided by the batch size; every cell is measured in a process of its own, because the heap that earlier cells leave behind changed the result. Each process runs with DOTNET_TC_CallCountingDelayMs=0 (call counting starts immediately instead of after the default delay), and no other DOTNET_ or COMPlus_ variable set, and every cell is first warmed up for 1000 ms of whole batches before BenchmarkDotNet's own warm-up, because a new process starts at the runtime's unoptimised tier. Memory: bytes allocated per task from BenchmarkDotNet's MemoryDiagnoser; allocation calls are not available. AMD Ryzen 9 7950X3D 16-Core Processor (125.0 GiB), Linux 7.0.0-34-generic (x86_64), measured on a shared workstation (32 logical CPUs; load 3.72 before, 4.52 after, at most 8.48 during). ## Writing a document back The `unchanged emit` rows time `YamlDocument.ToUtf8Bytes()` on a document that was not edited, and each call returns a new array. Where another library writes a file back faster, its `unchanged emit` row in [Results](#results) is marked ▼ loss, and the verdict in [Results at a glance](#results-at-a-glance) counts it. Most of the time in those rows is the operating system supplying fresh memory pages for the new output array right after a garbage collection, and every library in the run pays that cost the same way (the kit's [.NET harness notes](https://github.com/recursive-ai-oss/oneyaml-kit/blob/main/bench/csharp/README.md#time-and-memory) describe it). That is what this run measured on this host, not a bound. If you write large documents again and again, `WriteTo(IBufferWriter)` and `WriteTo(Stream)` write into a buffer you own and reuse, so they do not allocate a new output array on every call the way `ToUtf8Bytes()` does ([Input, output and lifetime](https://recursiveai.org/oneyaml/csharp/docs/editing/#input-output-and-lifetime)). The benchmarks do not measure those calls, so this page has no figure for them. ## Running the benchmarks yourself The published numbers come from the kit, which runs every library on the same workloads. To measure a checkout of this repository there, set `KIT_ONEYAML_CSHARP_SRC` to it and follow the kit's [benchmark notes](https://github.com/recursive-ai-oss/oneyaml-kit/blob/main/docs/benchmarks.md). This repository also has BenchmarkDotNet benchmarks in `bench/OneYaml.Benchmarks`, used while developing; their README says how to run them. Their results are not the published ones. # Conformance What oneyaml-csharp is measured against, the latest results, and every case it is known not to pass. The library targets YAML 1.2 with the Core schema. It is scored against the official [yaml-test-suite](https://github.com/yaml/yaml-test-suite), release `data-2022-01-17`, plus a set of cases written for the oneyaml family that cover edits, patches, writing, safety and diagnostics. Both run through the conformance kit of the oneyaml family, [oneyaml-kit](https://github.com/recursive-ai-oss/oneyaml-kit), which drives the library through its public API: `YamlDocument.Parse` and its nodes for events and values, `YamlSerializer` for writing, and `ApplyPatch` and `ApplyMergePatch` for patches. The kit holds the cases, the corrected expectations and the adapter for this library. From a checkout of the kit, run `bin/kit fast --adapter oneyaml-csharp` with `KIT_ONEYAML_CSHARP_SRC` set to a checkout of this repository. The tests in this repository that run the suite and the kit's cases through the library also run in CI, on every push to `main` and every pull request, against a pinned kit revision, so a change that breaks a case fails CI. Locally they are skipped unless `ONEYAML_KIT` names a kit checkout. ## Results The line below is generated by the kit's report tool from its result file for this library; it is not edited by hand. It gives: - the yaml-test-suite release and the pass counts against it, loose and strict, out of every case of that release; - for the cases where the kit corrects the upstream expectation, the pass counts against the corrected expectations, apart from the upstream counts; - the pass counts on the kit's own cases, and whether the slow cases were run. The tool refuses to write the line from a run that left cases out or that has a failure not on the deviation list below. To run the same check yourself, use the kit command above against a checkout of the release tag. yaml-test-suite data-2022-01-17: 680/681 loose, 586/681 strict. Against the kit's corrected expectations for 12 of them: 12/12 loose, 1/12 strict. Kit's own cases, all of them including the slow ones: 609/609 loose, 609/609 strict. [Details](https://recursiveai.org/oneyaml/csharp/docs/conformance/) ### How to read the counts - **Loose** counts the outcome only: valid input must give exactly the expected events, value or bytes, and invalid input must be reported as an error. - **Strict** counts everything the case states. For invalid input that includes the error category and position, and the events emitted before the error. - **Corrected** counts judge the cases where the upstream expectation is wrong against the kit's corrected one. The kit lists the corrections and their reasons. - **The kit's own cases** are counted apart from the suite. A run of the fast set leaves out the slow cases, and the line says so. ## Known deviations The cases the library does not pass, each with the expectation it was judged against (upstream or corrected), the mode (loose or strict), the outcome and the reason. The list is generated from the kit's deviation file for this library. The kit fails a run if a case fails that is not on the list, and also if an entry on the list starts to pass, so the list cannot go stale unnoticed. 2 known deviations: | Case | Expectation | Mode | Verdict | Reason | |---|---|---|---|---| | `yts:UGM3@value` | primary | loose | fail | The upstream value reads 450.00 as the integer 450; the library reads the float 450.0, which is right under the core schema (the corrected expectation passes). | | `yts:UGM3@value` | primary | strict | fail | The upstream value reads 450.00 as the integer 450; the library reads the float 450.0, which is right under the core schema (the corrected expectation passes). | ## What the results do not say - The suite covers the grammar. It is not a benchmark. - A pass is a pass of the kit's checks at the revision shown. It is not a proof of conformance for input the suite does not contain; the library is also fuzzed (`tests/OneYaml.Fuzz`). - The kit's own cases were written for this project family. They are a stricter and broader check than the upstream suite alone, and they are the project's judgment of correct behaviour where the specification leaves a choice, such as where a new mapping key goes. - The compatibility adapters are not scored by the kit. They are checked against the real YamlDotNet and SharpYaml by the differential tests in `tests/OneYaml.Compat.Tests`, and their differences are listed in [YamlDotNet differences](https://recursiveai.org/oneyaml/csharp/docs/compat-yamldotnet/) and [SharpYaml differences](https://recursiveai.org/oneyaml/csharp/docs/compat-sharpyaml/). # Stability What a release of oneyaml-csharp promises, and which .NET versions it supports. This is the only place this policy is stated; other pages link here. ## Versions From 1.0.0 the packages follow [Semantic Versioning](https://semver.org/). Within a major version: - No change breaks code that compiles against the public API of `OneYaml`, `OneYaml.Compat.YamlDotNet` or `OneYaml.Compat.SharpYaml`. That covers `YamlException` and its `Category`; the text of messages is written for people and may change in any release. - The text that `YamlSerializer.Serialize` writes with default options does not change, and neither do the bytes an edit leaves untouched, except to fix output that is incorrect. Such a fix is listed in the changelog. The public API and documented behaviour change only in a new major version. A fix for wrong behaviour can come in a minor or patch release, including a change to the exception an error path throws. When code could depend on the old behaviour, the [changelog](https://recursiveai.org/oneyaml/csharp/docs/changelog/) marks the fix **Breaking**, with what to write instead. ## Supported .NET versions The packages target `net8.0` and `net10.0`. CI builds and tests both on Linux, macOS and Windows. Dropping a target framework takes a new major version. Microsoft ends support for .NET 8 on 10 November 2026 ([.NET support policy](https://dotnet.microsoft.com/en-us/platform/support/policy/dotnet-core)). The `net8.0` target stays in every 1.x release all the same; it can go only in a new major version. After that date .NET 8 itself gets no more security fixes from Microsoft, so move to .NET 10 when you can. Security fixes go into the latest release; see [Security](https://github.com/recursive-ai-oss/oneyaml-csharp/blob/7818048c4be434333767c47d66176fc626d1da8e/SECURITY.md). # Changelog All notable changes are listed here, newest first. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and versions follow [Semantic Versioning](https://semver.org/). A change that can break your code is marked **Breaking** and says what to write instead. What a release promises, and the supported .NET versions, are under [Stability](https://recursiveai.org/oneyaml/csharp/docs/stability/). ## [Unreleased] ## [1.2.1] - 2026-10-18 ### Fixed - **A stream or reader too large for one buffer is a `Limit` error, whatever `MaxInputBytes` says.** With no input limit, or a limit above 2 GiB, a stream or reader larger than an array can hold, or one that never ends, could fail with another exception: - `YamlSerializer.Deserialize`, `DeserializeAll` and the compat readers over a `TextReader` threw `ArgumentOutOfRangeException` when no limit was set; - `OneYaml.Compat.SharpYaml` over a `Stream` threw `IOException` ("Stream was too long") or ran out of memory. They now stop with the usual limit error: a `YamlException` with category `Limit`, or in the compat packages the exception type that package already uses for a limit. With no limit set, a reader other than a `StringReader` (whose text is already in memory) is now read in chunks, as it already was under a limit, instead of in one call. - **`OneYaml.Compat.YamlDotNet` error marks after non-ASCII text count characters, not bytes.** `Start.Index` of a syntax error in text read from a string or a reader was the UTF-8 byte offset, so it was too large once a multi-byte character came before the error. It now counts UTF-16 characters, as YamlDotNet does, and a character outside the Basic Multilingual Plane counts as two columns, as in YamlDotNet. A leading byte order mark is still not counted. - **`OneYaml.Compat.SharpYaml` holds the whole text to `MaxInputBytes`, not only its first document.** A string, span, reader or stream with several documents was cut at the second document before the limit was checked, so `Deserialize("a: 1\n---\nb: 2\n", ...)` with `MaxInputBytes = 13` returned the first document instead of the limit error. The size of a string, span or reader is the UTF-8 size of its text without a leading byte order mark, as the core counts a string. A stream is held to its encoded size without the mark, and is read only up to the limit plus one buffer. - **`OneYaml.Compat.SharpYaml` error marks in a string that starts with a byte order mark are no longer shifted.** The marks (line, column and index) counted the mark as a character, and counted its bytes in the wrong place, so the same text gave different positions as a string and as a stream or reader. A string that began with a mark, a directive and more than one document could also read a wrong value (`a: 1` came back as `a: null`). The mark is now never a column, whatever the input. - **`OneYaml.Compat.YamlDotNet` marks of nodes and errors after a leading byte order mark are no longer shifted.** When the text came from a string, or from a reader that keeps the mark, `Start` and `End` of every node after the first, and of an error, were off by the mark. They now equal the marks of the same text without it. ## [1.2.0] - 2026-10-18 ### Added - **`OneYaml.Compat.YamlDotNet` tells an alias budget from a depth limit.** `AliasExpansionLimitExceededException` derives from `MaximumRecursionLevelReachedException` and is thrown where reading or writing expanded aliases passes the alias budget. Depth and size limits still throw the base type, and a `catch (MaximumRecursionLevelReachedException)` still catches both. `MaximumRecursionLevelReachedException` is no longer sealed so that this is possible. ### Changed - **A run of replacements on a large document no longer rewrites the offsets of everything behind each edit.** `ApplyPatch` and `ApplyMergePatch` edit one state in place, and each edit that kept the number of nodes moved the offsets of every later node: about 0.1 ms per edit at 100,000 keys, whatever the edit. On documents of 4,096 nodes or more, from the second edit of a patch on, that move is now noted per chunk of 64 nodes and carried out when a chunk is next read, so a run of edits costs what the edits touch. Measured, not a bound: parse, apply and write of 10,000 replacements on a mapping of 100,000 keys takes 0.15 s (was 1.05 s) and of 100,000 replacements 1.6 s (was 10.6 s); a 100-operation patch on an 8 MB document of records takes about 95 ms (was about 145 ms), mostly parsing. Output bytes, errors, positions and the stack a deep document needs are unchanged, and so is the memory a parsed document keeps. What still grows with the document: an edit that adds or removes nodes (`add`, `remove`, a collection value) moves the node array behind it, and an edit that changes the length of the text moves the text behind it; the comment and property tables are walked in full. - **A loop of single edits no longer copies the whole document for each edit.** (**Breaking** in one rare case: a document whose restore after a failed edit ran out of memory now refuses further use with `InvalidOperationException`; parse the text again to continue.) `Set`, `Remove`, the `YamlNode` indexers and the other single edits used to build a full copy of the document's state and swap it in, so each edit cost the size of the document. They now edit the document's own state in place. If an edit fails, the document is left as it was: most failures are found before anything changes, and an edit that fails after it has begun changing the text parses the old text again, so only that error path pays for it. The one exception is running out of memory while the old text is parsed again: the error you get is the edit's own, and the document then refuses further use with an `InvalidOperationException` (`Dispose` still works). Any other failure during that step would be a defect in the library and is not hidden. Output bytes, errors, positions and live views are unchanged. Measured, not a bound (parse and write included): 1,000 edits on a mapping of 100,000 keys take 0.09 s through the indexer (was 0.23 s) and 0.18 s through `Set` (was 0.24 s); 10,000 take 0.8 s (was 2.3 s); 1,000 edits on the 8 MB records file take 0.9 s (was 4.7 s) and 10,000 take 8.2 s (was 46 s); setting one field on every item of a 1 MB records file takes 0.35 s (was 0.7 s). One `Set` on a parsed document is 1.0 to 2.6 times as fast on the benchmark files and allocates about 120 bytes less; parsing, reading and writing back are as fast as before and allocate the same. What still grows with the document: an edit moves the offsets of the nodes behind it, an edit that adds or removes nodes moves the node array, an edit that changes the length of the text moves the text behind it, and the comment and property tables are walked in full. `ApplyPatch` and `ApplyMergePatch` hold some of that move back across a run of edits and still do, so a patch of many operations stays faster than the same edits made one call at a time. - **A key index lost to a race is built again.** When two readers raced to index two large mappings, one could leave its table in the slot of the other, and the other mapping stayed unindexed until an edit. The slot now counts as free. ### Fixed - **A text reader is no longer read to its end before the input limit applies.** `YamlSerializer.Deserialize` and `DeserializeAll` over a `TextReader` read the whole reader and only then compared its size with `MaxInputBytes`, so a very large or endless reader filled memory first, even under `YamlLimits.Untrusted`. They now read in chunks, count the UTF-8 size as they go and stop with the usual `Limit` error as soon as the text is over the limit, after reading at most one chunk (4,096 characters) beyond it. A text exactly at the limit still reads, and a leading byte order mark is not counted, as for a string. When no input size limit is set (`MaxInputBytes` left at its default, as in `YamlLimits.Default` and `YamlLimits.None`), the reader is read in one call as before. - **Compat readers over a `TextReader` apply the input limit as the core does.** `YamlStream.Load(TextReader, limits)` and the other YamlDotNet readers stopped reading at `MaxInputBytes` characters and parsed what they had, so a text with a byte order mark that was one character over the limit lost its tail without an error; it is now a `Limit` error. The SharpYaml `Deserialize` overloads over a `TextReader` refused a text of exactly the limit when it began with a byte order mark, which a string of the same text is allowed; they now accept it. Both use the core's chunked reader. - **`OneYaml.Compat.YamlDotNet` writes `Int128`, `UInt128` and `Half` as numbers and reads them back.** They were written as `{}` (as YamlDotNet does), which lost the value. A `Half` too large for the type is an error rather than infinity. - **Breaking: `OneYaml.Compat.YamlDotNet` reports a repeated key read from a parser as a `YamlException`.** `YamlStream.Load(IParser)` threw `ArgumentException`; it now throws the library's `YamlException`, positioned at the repeated key, as YamlDotNet itself does. Code that caught `ArgumentException` around `Load(IParser)` should catch `YamlException` instead. - **A key in extension data now beats the same key merged into the same merge source.** With `MergeKeys` on, a mapping read through a merge key (`<<: *base`) whose own `<<` brought in `x` and which also gave `x` itself kept the merged value when the type has an extension data member; the mapping's own `x` now wins, wherever its `<<` stands, as it already did for members and dictionaries. The first source still wins among several, and the keys of the mapping that is read itself still beat every source. - **`OneYaml.Compat.YamlDotNet` no longer reads the error text to tell an alias budget from a depth limit.** The code that refuses the expansion marks the core error, so rewording a message cannot change which exception is thrown. ## [1.1.1] - 2026-10-17 ### Changed - **Many operations on one large mapping no longer slow down with its size.** Looking up a key, working out the indentation to write and checking for duplicate keys each walked the whole mapping on every operation. On a mapping of 100,000 keys, 10,000 patch operations took 19.1 s to remove, 11.9 s to replace and 3.4 s to test; they take about 1.5 s, 1.1 s and 26 ms now (`ApplyPatch`, `ApplyMergePatch`, `Set`, `Remove` and the indexers share the change). A mapping of 256 or more entries gets a key index once searching it in order has cost a few times its size, so a small mapping, or one read once or twice, behaves and costs as before; the read that builds the index costs a few searches more, and later reads are quicker; the index stays correct across edits, and a mapping with two keys of the same text is searched in order as before. Output bytes, errors and positions are unchanged. A document that is only parsed keeps its size; once an index is built the document holds it until it is disposed, about 13 to 32 bytes per entry (21 at 100,000 keys). What still grows with the document is moving the entries after an edit, about 80 microseconds per edit at 100,000 keys. - **Faster number reads.** Plain integers of up to 18 digits and plain decimal numbers with a short mantissa and exponent are read in one pass, in typed and generic loads and wherever a float key is compared. Loading number-heavy input is 5 to 14 percent faster; every value, error, category and position is unchanged, and hexadecimal, octal, `.inf`, `.nan`, long and out-of-range numbers take the same path as before. ### Fixed - **Error positions in UTF-16 and UTF-32 input.** A malformed UTF-16 or UTF-32 input, a lone surrogate for example, was reported one column too far, with or without a byte order mark; the column now names the bad character. The `Offset` of such an error still counts bytes of the original input, the byte order mark included. Other errors in these encodings already had the column and offset of the same text read as UTF-8, and a test now holds that. ## [1.1.0] - 2026-10-17 ### Changed - **`Get` and `GetValue` read collections.** Asking a node for `object`, through `YamlDocument.Get`, `TryGet`, `YamlNode.GetValue` or `TryGetValue`, now gives what `YamlSerializer.Deserialize` gives: a `Dictionary` for a mapping and a `List` for a sequence. It used to fail with "The node is not a scalar". - **A reader that was copied and left behind says so.** A `Utf8YamlReader` shares its scanner with its copies. When a copy read past the end of its value, the original used to fail with a syntax error that blamed the input. It now throws `InvalidOperationException` that names the misuse; the same holds for a copy used after the original read on. Looking ahead on a copy inside the current value still works. The failure is reported when the reader hits the damaged position, so a copy that damaged a reader without causing an error stays undetected: do not copy the reader. - **Faster write-back of an unedited document.** `YamlDocument.ToUtf8Bytes` no longer clears the output array before copying into it. - **Error messages.** A refused alias expansion reads the same from a document and from the serializer, and names a position. A key discriminator that is missing points at the mapping, not at its first key. The SharpYaml adapter's messages state the position once. ### Fixed - **An explicit key beats a merged one inside a merge source.** With `MergeKeys` on, a mapping used as the source of a `<<` key could hold its own `<<` and then an explicit key, as in `{<<: {a: 1}, a: 2}`. Reached through the outer merge, a typed load into a class, record or dictionary gave `a` the merged value 1. It now gives 2, as when that mapping is read directly and as generic load does. The first source still wins among several, and a repeated key inside one source is still an error. - **A repeated key inside a merge source is an error.** With `MergeKeys` on, a mapping used as the source of a `<<` key could repeat a key that binds to a member or a dictionary entry, and the second value was dropped without a word. It now fails with `DuplicateKey` at the repeated key, as a repeated key does anywhere else. The same key in two different sources is still fine; the first source wins. - **The YamlDotNet adapter's `Serializer` bounds a graph that shares its parts.** With `DisableAliases()` or `JsonCompatible()`, a graph in which objects are reachable many times over was written out in full without limit. The nodes written for repeated objects are now held to the alias budget of the limits and the writer throws `MaximumRecursionLevelReachedException` beyond it. `SerializerBuilder.WithLimits` sets the limits; the default is `YamlLimits.Default`. - The documentation of `MaxDepth` in the SharpYaml adapter now matches what it does: the property reads back the value set, and the lower of it and `Limits.MaxDepth` applies. ## [1.0.0] - 2026-10-16 The first release: a YAML 1.2 library for .NET 8 and .NET 10 that reads and writes .NET types, and edits a file while keeping its comments and layout. From this release the public API follows the promises under [Stability](https://github.com/recursive-ai-oss/oneyaml-csharp/blob/v1.0.0/README.md#stability). ### Added - **The `OneYaml` package.** - `YamlSerializer` reads YAML into .NET types and writes .NET values as YAML, in the shape of System.Text.Json: `YamlSerializerOptions`, naming policies (camel case by default), custom converters, polymorphism by key or by tag, extension data, and the System.Text.Json attributes where no YAML attribute is present. A source generator in the package writes metadata for trimmed and native AOT applications. See [Serializer](https://github.com/recursive-ai-oss/oneyaml-csharp/blob/v1.0.0/docs/serializer.md). - `YamlDocument` edits a file through `YamlNode` indexers or RFC 6901 pointer methods and keeps every byte you do not change. A new value is written in the style of the old one, or the nearest style that can hold it. See [Editing](https://github.com/recursive-ai-oss/oneyaml-csharp/blob/v1.0.0/docs/editing.md). - `ApplyPatch` (JSON Patch, RFC 6902) and `ApplyMergePatch` (JSON Merge Patch, RFC 7396) change a `YamlDocument` atomically and keep its layout. See [Patches](https://github.com/recursive-ai-oss/oneyaml-csharp/blob/v1.0.0/docs/patches.md). - `YamlConverter` with `Utf8YamlReader` for custom types, and `Utf8YamlEmitter` to write YAML token by token. See [Reader and emitter](https://github.com/recursive-ai-oss/oneyaml-csharp/blob/v1.0.0/docs/reader-emitter.md). - `YamlException` reports bad or oversized input with a stable `Category`, the line, column and byte offset, and the path of a value that failed to bind. - **The `OneYaml.Compat.YamlDotNet` package**, for code written against YamlDotNet 18: the serializer builders and the representation model under `OneYaml.Compat.YamlDotNet` namespaces. See [Migrating from YamlDotNet](https://github.com/recursive-ai-oss/oneyaml-csharp/blob/v1.0.0/docs/migrate-yamldotnet.md) and the [differences from YamlDotNet](https://github.com/recursive-ai-oss/oneyaml-csharp/blob/v1.0.0/docs/compat/yamldotnet.md). - **The `OneYaml.Compat.SharpYaml` package**, for code written against the SharpYaml 3 serializer: `YamlSerializer`, `YamlSerializerOptions` and the attributes under the `OneYaml.Compat.SharpYaml` namespace. See [Migrating from SharpYaml](https://github.com/recursive-ai-oss/oneyaml-csharp/blob/v1.0.0/docs/migrate-sharpyaml.md) and the [differences from SharpYaml](https://github.com/recursive-ai-oss/oneyaml-csharp/blob/v1.0.0/docs/compat/sharpyaml.md). - **Limits.** `YamlLimits` has the presets `Default`, `Untrusted` and `None`. `Default` applies whenever you pass no limits and bounds nesting depth, alias expansion and the digits of an integer. `Untrusted` also bounds input size, scalar size and documents per stream; use it for input you do not control. The adapters read with `Default` unless you give them limits through members their targets do not have: `WithLimits`, and limits overloads of `YamlStream.Load` and the `Parser` constructor, for YamlDotNet code; `YamlSerializerOptions.Limits` for SharpYaml code. See [Limits](https://github.com/recursive-ai-oss/oneyaml-csharp/blob/v1.0.0/docs/limits.md). ### Behaviour to know before you upgrade from another library - **YAML 1.2 Core schema by default.** Plain `yes` is a string, `0755` is the integer 755, a key written twice in a mapping is an error, and `<<` merges only with the `MergeKeys` option. The compatibility adapters read the YAML 1.1 forms their targets document. - **`test` in a patch compares YAML values, not JSON values.** An integer and a float are different values, so `1` does not equal `1.0`, and a plain key `80` is an integer that does not match a member `"80"`. See [Differences from RFC 6902](https://github.com/recursive-ai-oss/oneyaml-csharp/blob/v1.0.0/docs/patches.md#differences-from-rfc-6902). - **What an edit keeps.** Removing an entry also removes the comment lines just above it, replacing a whole mapping or sequence discards the comments inside it, and writes through an alias are refused. There is no API to read or write comments. See [What an edit keeps](https://github.com/recursive-ai-oss/oneyaml-csharp/blob/v1.0.0/docs/editing.md#what-an-edit-keeps-and-what-it-does-not). - **The adapters refuse what they cannot do.** Options an adapter cannot follow, such as YamlDotNet's `EnsureRoundtrip` and SharpYaml's layout options, throw `NotSupportedException` instead of being ignored. Each is listed on the adapter's differences page. [Unreleased]: https://github.com/recursive-ai-oss/oneyaml-csharp/compare/v1.2.1...HEAD [1.2.1]: https://github.com/recursive-ai-oss/oneyaml-csharp/compare/v1.2.0...v1.2.1 [1.2.0]: https://github.com/recursive-ai-oss/oneyaml-csharp/compare/v1.1.1...v1.2.0 [1.1.1]: https://github.com/recursive-ai-oss/oneyaml-csharp/compare/v1.1.0...v1.1.1 [1.1.0]: https://github.com/recursive-ai-oss/oneyaml-csharp/compare/v1.0.0...v1.1.0 [1.0.0]: https://github.com/recursive-ai-oss/oneyaml-csharp/releases/tag/v1.0.0 # Parse YAML into C# classes 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: ```csharp 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(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("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 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](https://recursiveai.org/oneyaml/csharp/docs/serializer/)). When the shape isn’t known ahead of time, read nodes from a `YamlDocument`: [read YAML without a class](https://recursiveai.org/oneyaml/csharp/cookbook/read-yaml-without-a-class/). # Read YAML without a class When you don’t have a type for the data, parse it into a `YamlDocument` and read values by JSON Pointer, or walk its nodes the way you walk a `JsonNode`. ```csharp using OneYaml; using var doc = YamlDocument.Parse(""" server: host: 0.0.0.0 port: 8080 # behind the load balancer features: [auth, metrics] limits: cpu: 0.5 memory: 512Mi """); // Read one value into a .NET type, by JSON Pointer. Console.WriteLine($"port: {doc.Get("/server/port")}"); // Or walk the nodes, like JsonNode: values, kinds and positions. YamlNode port = doc.Root["server"]["port"]; Console.WriteLine($"port {port.GetValue()} at {port.Position.Line}:{port.Position.Column}"); foreach (YamlNode feature in doc.Root["features"].Items) { Console.WriteLine($"feature: {feature.GetValue()}"); } foreach (var (key, value) in doc.Root["limits"].Entries) { Console.WriteLine($"limit {key.GetValue()} = {value.GetValue()} ({value.Kind})"); } // A missing path: check first, or use TryGet. Console.WriteLine(doc.Contains("/server/tls")); Console.WriteLine(doc.TryGet("/server/tls", out bool tls) ? $"tls: {tls}" : "tls not set"); ``` ```text port: 8080 port 8080 at 3:9 feature: auth feature: metrics limit cpu = 0.5 (Scalar) limit memory = 512Mi (Scalar) False tls not set ``` ## Good to know - `doc.Get(pointer)` reads the node at a JSON Pointer into any type, with the serializer’s rules. `TryGet` and `Contains` check without throwing. - `node["key"]` and `node[0]` index mappings and sequences. `Items` and `Entries` enumerate them in file order. - `Position` is where the node’s content starts: a 1-based line and column, and the offset. - `Kind`, `Style`, `Tag` and `Anchor` describe the node as written. `GetValue` reads a scalar; `Deserialize` reads a whole subtree. - Every pointer method takes a `document` index for multi-document files, 0 by default ([multi-document files](https://recursiveai.org/oneyaml/csharp/cookbook/kubernetes-multi-document/)). # Edit YAML and keep its comments Change a YAML file from C# the way a person would: one value, one list item, one new key. Every comment, blank line and quote elsewhere stays as it was. ```csharp using OneYaml; using var doc = YamlDocument.Parse(""" # App settings, edited by hand and by tools. server: host: 0.0.0.0 port: 8080 # behind the load balancer # Feature flags features: - auth - legacy-export # remove after the 2.0 release - metrics """); doc.Set("/server/port", 9090); // its trailing comment stays doc.Remove("/features/1"); // its comment goes with it doc.Root["server"]["timeout"] = "30s"; // a new key, laid out like its siblings doc.Add("/features/-", "audit-log"); // appended in the list's own style Console.Write(doc); ``` ```text # App settings, edited by hand and by tools. server: host: 0.0.0.0 port: 9090 # behind the load balancer timeout: 30s # Feature flags features: - auth - metrics - audit-log ``` ## Good to know - **Each value keeps its style.** `Set` writes in the old value’s style when it can hold the new value: plain stays plain, quotes stay, and a block literal stays a block literal. Otherwise only that value falls back to a style that can. - **Comments are kept, not modeled.** The comment after a changed value stays, and a removed item takes its comment with it. There is no API to read, add or move comments. - **New keys** go at the end of their mapping and copy its layout. `Add("/list/-", value)` appends to a sequence, and an index equal to the count appends too. - **An edit through an alias is refused**, with `YamlErrorCategory.RefusedAlias`, because it would change every place that shares the node ([anchors and aliases](https://recursiveai.org/oneyaml/csharp/cookbook/anchors-aliases-merge-keys/)). - A new plain string such as `yes` stays plain under YAML 1.2. If YAML 1.1 programs also read the file, set `QuoteYaml11Lookalikes = true` in `YamlDocumentOptions` so such strings are quoted. What an edit keeps, and what it doesn’t, is in [Editing](https://recursiveai.org/oneyaml/csharp/docs/editing/). To change many values from a JSON document, use a patch: [merge or patch YAML](https://recursiveai.org/oneyaml/csharp/cookbook/patch-yaml/). The same edit in Go, Rust and C#: [Edit YAML without losing comments](https://recursiveai.org/oneyaml/articles/edit-yaml-keep-comments/). # Merge or patch YAML Overlay one document on another, or apply a JSON Patch, without losing the file’s comments and layout. A Merge Patch suits environment overrides; a JSON Patch suits scripted changes with a `test` guard. ```csharp using OneYaml; using var doc = YamlDocument.Parse(""" # values.yaml image: repository: registry.example.com/web tag: "1.4.2" # set by CI replicas: 2 resources: limits: cpu: 500m memory: 256Mi """); // An environment overlay as a merge patch: objects merge, null deletes. doc.ApplyMergePatch("""{ "replicas": 5, "resources": { "limits": { "cpu": null, "memory": "1Gi" } } }"""); Console.Write(doc); // A JSON Patch with a guard, built in code: `test` must pass or nothing changes. var bump = new YamlPatch() .Test("/image/tag", "1.4.2") .Replace("/image/tag", "1.5.0"); doc.ApplyPatch(bump); Console.WriteLine($"tag now {doc.Get("/image/tag")}"); // Applied again, the test fails and the document is left as it was. try { doc.ApplyPatch(bump); } catch (YamlException e) when (e.Category == YamlErrorCategory.PatchTest) { Console.WriteLine($"second run refused at operation {e.OperationIndex}: {e.Message}"); } ``` ```text # values.yaml image: repository: registry.example.com/web tag: "1.4.2" # set by CI replicas: 5 resources: limits: memory: 1Gi tag now 1.5.0 second run refused at operation 0: The value at '/image/tag' is not the value the test expects. ``` ## Good to know - **Atomic.** If any operation fails, the document is unchanged. The `YamlException` has a `Category` (`PatchTest` for a failed test) and an `OperationIndex`. - **A Merge Patch merges mappings and replaces everything else.** `null` deletes a key; a list in the patch replaces the whole list. - **Only the named values change.** The `# set by CI` comment and the quotes around the tag stay. - **YAML rules, not JSON rules.** `test` compares YAML values, so `1` and `1.0` differ. A value moved by `move` is written in the style of its new place, without its old comments. - `ApplyPatch` takes JSON text, UTF-8 bytes, a `JsonNode` or a `YamlPatch`; `ApplyMergePatch` takes JSON text, UTF-8 bytes or a `JsonNode`. All six operations are supported. Every rule is in [Patches](https://recursiveai.org/oneyaml/csharp/docs/patches/). A longer guide: [Apply a JSON Patch to a YAML file](https://recursiveai.org/oneyaml/articles/json-patch-yaml/). # Convert YAML to JSON and back Turn a YAML file into JSON, or JSON into YAML. `YamlSerializer` reads into and writes from `JsonNode` and `JsonElement`, so System.Text.Json does the JSON side. ```csharp using System.Text.Json; using System.Text.Json.Nodes; using OneYaml; // YAML to JSON: read into a JsonNode, write it with System.Text.Json. JsonNode? json = YamlSerializer.Deserialize(""" name: web ports: [80, 443] tls: true owner: ~ """); Console.WriteLine(json!.ToJsonString(new JsonSerializerOptions { WriteIndented = true })); // JSON to YAML: JSON is valid YAML 1.2, so read it and write YAML. JsonNode? fromJson = JsonNode.Parse("""{"name": "web", "ports": [80, 443], "env": {"LOG": "info"}}"""); Console.Write(YamlSerializer.Serialize(fromJson)); ``` ```text { "name": "web", "ports": [ 80, 443 ], "tls": true, "owner": null } name: web ports: - 80 - 443 env: LOG: info ``` ## Good to know - The keys keep the file’s order in both directions. - `JsonNode`, `JsonObject`, `JsonArray`, `JsonValue` and `JsonElement` are all supported targets. `JsonDocument` isn’t, because a member of a model can’t dispose it; use `JsonElement`. - JSON is valid YAML 1.2, so `YamlSerializer.Deserialize` and `YamlDocument.Parse` read JSON text directly. - For typed data, skip the tree: deserialize YAML into your type and serialize it with `JsonSerializer`. # Read Kubernetes manifests and multi-document YAML A Kubernetes manifest often holds several documents separated by `---`. Read each one into a record with `DeserializeAll`, or edit one of them in a `YamlDocument` and write the whole file back with its comments. The file: ```yaml # Web frontend apiVersion: apps/v1 kind: Deployment metadata: name: web spec: template: spec: containers: - name: web image: registry.example.com/web:1.4.2 # pinned by release tooling --- apiVersion: v1 kind: Service metadata: name: web spec: ports: - port: 80 targetPort: 8080 ``` The program: ```csharp using OneYaml; string src = File.ReadAllText("app.yaml"); // Typed: every document of the stream, in order. using (var reader = new StringReader(src)) { foreach (var obj in YamlSerializer.DeserializeAll(reader)) { Console.WriteLine($"{obj!.Kind} {obj.Metadata.Name} ({obj.ApiVersion})"); } } // Lossless: edit the Deployment, keep everything else byte for byte. using var doc = YamlDocument.Parse(src); int deploy = Enumerable.Range(0, doc.Documents.Count).First(i => doc.Get("/kind", i) == "Deployment"); doc.Set("/spec/template/spec/containers/0/image", "registry.example.com/web:1.5.0", deploy); Console.Write(doc); record K8sObject(string ApiVersion, string Kind, Metadata Metadata); record Metadata(string Name); ``` ```text Deployment web (apps/v1) Service web (v1) # Web frontend apiVersion: apps/v1 kind: Deployment metadata: name: web spec: template: spec: containers: - name: web image: registry.example.com/web:1.5.0 # pinned by release tooling --- apiVersion: v1 kind: Service metadata: name: web spec: ports: - port: 80 targetPort: 8080 ``` ## Good to know - `YamlSerializer.DeserializeAll` reads every document from a `TextReader` or a `Stream`; `DeserializeAllAsync` streams them as an `IAsyncEnumerable`. `SerializeAll` writes a list back as a stream. - A `YamlDocument` holds the whole stream. `Documents` lists the root of each, `Root` is the first, and every pointer method and patch takes a `document` index, 0 by default. Output always writes the whole stream. - For manifests from outside, set `MaxDocuments` and the other limits ([read untrusted YAML safely](https://recursiveai.org/oneyaml/csharp/cookbook/untrusted-input/)). # Report errors with line and column Validate a YAML file and say exactly what’s wrong and where. Every OneYaml error is a `YamlException` with the position, a category to switch on and, for a value, the JSON Pointer path to it. ```csharp using OneYaml; // Malformed YAML: line, column and a category to switch on. Report(() => YamlDocument.Parse("name: web\nports: [80, 443\n").Dispose()); // A duplicate key. Report(() => YamlSerializer.Deserialize("name: web\nname: api\n")); // A value that doesn't fit its type: the path to it, too. Report(() => YamlSerializer.Deserialize("port: 99999\ndebug: yes\n")); static void Report(Action read) { try { read(); } catch (YamlException e) { Console.WriteLine($"{e.Line}:{e.Column} {e.Category} {e.Path}"); Console.WriteLine($" {e.Message}"); } } record Config(ushort Port, bool Debug); ``` ```text 3:1 Syntax Expected ',' or ']' in the flow sequence. (line 3, column 1) 2:1 DuplicateKey /name The key 'name' appears twice in the mapping. (line 2, column 1, path /name) 1:7 Binding /port The number '99999' is outside the range of an unsigned 16-bit integer. (line 1, column 7, path /port) ``` ## Good to know - `Line` and `Column` are 1-based; `Offset` is the position in the input. `Path` is the JSON Pointer to the value, when there is one. - **Switch on `Category`**, not on the message: `Syntax`, `DuplicateKey`, `Binding`, `Limit`, `PatchTest`, `RefusedAlias` and the others. - `Message` already includes the position and path, so it reads well in a log. Set `SourceName` in the options to put the file name in front of it. - A read stops at the first error. To check every key a type doesn’t have, set `UnmappedMemberHandling = YamlUnmappedMemberHandling.Disallow` ([parse YAML into classes](https://recursiveai.org/oneyaml/csharp/cookbook/parse-yaml-into-classes/)). # Anchors, aliases and merge keys Anchors (`&name`) and aliases (`*name`) reuse a node. Merge keys (`<<`) copy a mapping’s entries into another, as docker-compose and many CI files do. YAML 1.2 has no merge keys, so OneYaml reads `<<` as a plain key unless you set `MergeKeys = true`. ```csharp using OneYaml; const string Src = """ defaults: &defaults image: nginx:1.27 replicas: 2 web: <<: *defaults replicas: 3 worker: *defaults """; // Aliases are expanded on read; `<<` is a plain key in YAML 1.2. var plain = YamlSerializer.Deserialize>>(Src)!; Console.WriteLine($"worker.image = {plain["worker"]["image"]}"); Console.WriteLine($"web has image without merge keys: {plain["web"].ContainsKey("image")}"); // MergeKeys reads docker-compose-style files the way YAML 1.1 tools do. var merged = YamlSerializer.Deserialize>(Src, new YamlSerializerOptions { MergeKeys = true })!; Console.WriteLine($"web = {merged["web"]}"); // A document keeps anchors and aliases as written, and can follow them. using var doc = YamlDocument.Parse(Src); YamlNode worker = doc.Root["worker"]; Console.WriteLine($"worker is {worker.Kind}, target &{worker.Target.Anchor}, image {doc.Get("/worker/image")}"); // An edit through an alias would change every place that shares it, so it is refused. try { doc.Set("/worker/replicas", 5); } catch (YamlException e) when (e.Category == YamlErrorCategory.RefusedAlias) { Console.WriteLine(e.Message); } record Svc(string Image, int Replicas); ``` ```text worker.image = nginx:1.27 web has image without merge keys: False web = Svc { Image = nginx:1.27, Replicas = 3 } worker is Alias, target &defaults, image nginx:1.27 The edit would write through an alias and change every place that shares the anchored node. ``` ## Good to know - Keys in the mapping win over merged ones: `web` has 3 replicas, not 2. - **Alias expansion is bounded by default**, so a “billion laughs” file fails with a `Limit` error instead of exhausting memory ([read untrusted YAML safely](https://recursiveai.org/oneyaml/csharp/cookbook/untrusted-input/)). - **A `YamlDocument` keeps `&` and `*` as written.** An alias node has `Kind` `Alias`, `Target` follows it, and `Anchor` is a node’s own anchor. Reads through an alias, such as `Get("/worker/image")`, follow it. - An edit below an alias is refused with `RefusedAlias`, because it would change every place that shares the node. Replacing the alias node itself is allowed. Merge keys across Go, Rust and C# libraries, measured: [YAML anchors, aliases and merge keys explained](https://recursiveai.org/oneyaml/articles/yaml-anchors-aliases-merge-keys/). # Read untrusted YAML safely YAML from a request body, an upload or another tenant can be built to exhaust memory or the stack. OneYaml bounds depth, alias expansion and integer size by default; for input you don’t control, use `YamlLimits.Untrusted`, which also bounds input size, scalar size and document count. ```csharp using System.Text; using OneYaml; var options = new YamlSerializerOptions { Limits = YamlLimits.Untrusted }; // A normal request body reads as usual. var ok = YamlSerializer.Deserialize>("name: web\nports: [80]\n", options)!; Console.WriteLine($"ok: {ok.Count} keys"); // "Billion laughs": nine levels of aliases, each repeated ten times. var bomb = new StringBuilder("l0: &l0 [x, x, x, x, x, x, x, x, x, x]\n"); for (int i = 1; i < 9; i++) { bomb.Append($"l{i}: &l{i} [{string.Join(", ", Enumerable.Repeat($"*l{i - 1}", 10))}]\n"); } Report(() => YamlSerializer.Deserialize(bomb.ToString(), options)); // Deep nesting is refused too, without overflowing the stack. Report(() => YamlSerializer.Deserialize(new string('[', 100_000), options)); // Size the limits yourself: start from the preset and change what you need. var small = new YamlSerializerOptions { Limits = YamlLimits.Untrusted with { MaxInputBytes = 1024 } }; Report(() => YamlSerializer.Deserialize(string.Concat(Enumerable.Repeat("a: b\n", 1000)), small)); static void Report(Action read) { try { read(); } catch (YamlException e) when (e.Category == YamlErrorCategory.Limit) { Console.WriteLine($"refused: {e.Message}"); } } ``` ```text ok: 2 keys refused: Expanding the aliases exceeds the budget set by YamlLimits (AliasNodeFactor 2, AliasNodeFloor 100000, AliasByteFactor 2, AliasByteFloor 8388608). (line 5, column 45) refused: The nesting depth is larger than the limit MaxDepth (128). (line 1, column 129) refused: The input is larger than the limit MaxInputBytes (1024). ``` ## Good to know - `YamlLimits` is a record: start from `YamlLimits.Untrusted` and change only what your input needs with `with { ... }`. - The same limits apply to `YamlDocument.Parse` through `YamlDocumentOptions.Limits`. - An alias whose expansion would exceed the budget is refused, and the message names the settings that set it. - A limit error has `Category == YamlErrorCategory.Limit`, so you can tell it apart from bad YAML. - No unsafe code and no dependencies outside .NET, and the parser is fuzzed. Every limit and its default is in [Limits](https://recursiveai.org/oneyaml/csharp/docs/limits/). # Use YAML in a native AOT app The `YamlSerializer` overloads that take options use reflection, which trimming and native AOT can’t rely on. Declare a `YamlSerializerContext` and pass its generated metadata instead. The generator ships in the `OneYaml` package; there is nothing else to install. ```xml true ``` ```csharp using OneYaml; // Generated metadata instead of reflection: safe to trim and to publish as native AOT. string src = """ retries: 3 endpoint: https://example.com tags: [web, public] """; Settings settings = YamlSerializer.Deserialize(src, AppYamlContext.Default.Settings)!; Console.WriteLine($"{settings.Retries} retries against {settings.Endpoint.Host}"); Console.Write(YamlSerializer.Serialize(settings with { Retries = 5 }, AppYamlContext.Default.Settings)); // YamlDocument takes generated metadata too. using var doc = YamlDocument.Parse(src); doc.Set("/tags", new List { "web", "internal" }, AppYamlContext.Default.ListString); Console.WriteLine(doc); record Settings(int Retries, Uri Endpoint, List Tags); [YamlSerializable(typeof(Settings))] partial class AppYamlContext : YamlSerializerContext; ``` ```text 3 retries against example.com retries: 5 endpoint: https://example.com tags: - web - public retries: 3 endpoint: https://example.com tags: [web, internal] ``` That output is from the native executable built by `dotnet publish -c Release`, with warnings treated as errors. ## Good to know - The generator writes a `YamlTypeInfo` property on the context for each `[YamlSerializable]` type and for every type those reach, such as `ListString` for `List` above. - Generated and reflection metadata bind the same way: attributes, converters, polymorphism and extension data work with both. - For options other than the defaults, create the context with them, `new AppYamlContext(new YamlSerializerOptions { ... })`, or set `TypeInfoResolver = AppYamlContext.Default` on options you already pass around. - With `PublishTrimmed` or `PublishAot`, reflection-based binding is off by default. Set the MSBuild property `YamlSerializerIsReflectionEnabledByDefault` to `true` to keep it. - Mistakes in a context, such as a context that isn’t `partial`, are build-time diagnostics `OY0001` to `OY0010`. More in [source generation and native AOT](https://recursiveai.org/oneyaml/csharp/docs/serializer/#source-generation-and-native-aot). Trim warnings, publish results and YamlDotNet compared: [C# YAML with native AOT](https://recursiveai.org/oneyaml/articles/csharp-yaml-native-aot/). # Switch from YamlDotNet with one package `OneYaml.Compat.YamlDotNet` is a subset of YamlDotNet’s API on top of OneYaml: the serializer builders and the representation model, tested against YamlDotNet 18.1.0. Swap the package and the namespaces, and the builder calls stay. ```sh dotnet remove package YamlDotNet dotnet add package OneYaml.Compat.YamlDotNet --version 1.2.1 ``` ```csharp using OneYaml.Compat.YamlDotNet.Serialization; // was: YamlDotNet.Serialization using OneYaml.Compat.YamlDotNet.Serialization.NamingConventions; // was: YamlDotNet.Serialization.NamingConventions // The same builders and calls as YamlDotNet. IDeserializer deserializer = new DeserializerBuilder() .WithNamingConvention(CamelCaseNamingConvention.Instance) .IgnoreUnmatchedProperties() .Build(); var service = deserializer.Deserialize("name: web\nreplicas: 2\nlegacy: true\n"); Console.WriteLine($"{service.Name} x{service.Replicas}"); ISerializer serializer = new SerializerBuilder() .WithNamingConvention(CamelCaseNamingConvention.Instance) .Build(); Console.Write(serializer.Serialize(service)); class Service { public string Name { get; set; } = ""; public int Replicas { get; set; } } ``` ```text web x2 name: web replicas: 2 ``` ## Good to know - **Not covered:** static contexts, type inspectors, node deserializers, the scanner and comment events. The [package page](https://recursiveai.org/oneyaml/csharp/docs/compat-yamldotnet/) lists what is covered and every difference. - The compatibility package is a bridge. For new code, `YamlSerializer` and `YamlDocument` in the `OneYaml` package read YAML 1.2, keep comments when you edit, and support native AOT. - From SharpYaml, the bridge is `OneYaml.Compat.SharpYaml` ([package page](https://recursiveai.org/oneyaml/csharp/docs/compat-sharpyaml/), [migration guide](https://recursiveai.org/oneyaml/csharp/docs/migrate-sharpyaml/)). What to check after the swap, and how to move to the `OneYaml` API itself, are in [Migrating from YamlDotNet](https://recursiveai.org/oneyaml/csharp/docs/migrate-yamldotnet/). A longer guide: [Replacing YamlDotNet](https://recursiveai.org/oneyaml/articles/yamldotnet-alternative/).