# oneyaml Go docs, all pages (oneyaml-go 1.0.0) Every page of https://recursiveai.org/oneyaml/go/docs/ and https://recursiveai.org/oneyaml/go/cookbook/ as markdown, getting started first. Index: https://recursiveai.org/oneyaml/go/llms.txt # Getting started with oneyaml for Go Install oneyaml for Go, read YAML into Go structs, write it back, edit a file in place without losing its comments, and apply JSON Patch and merge patches. oneyaml-go is pure-Go YAML 1.2 that edits a file and keeps its comments. Its `Marshal` and `Unmarshal` work the way `encoding/json` does, and a `Document` changes only the values you set. Every sample on this page was compiled and run against `oneyaml.dev/go` v1.0.0; the output under each one is what it printed. ## Install oneyaml needs Go 1.24 or newer. ```sh mkdir yaml-start && cd yaml-start go mod init example.com/yaml-start go get oneyaml.dev/go@v1.0.0 ``` The import path is `oneyaml.dev/go` and the package name is `oneyaml`. It has no dependencies outside the standard library, no `unsafe` and no cgo. The license is MIT or Apache-2.0, at your choice. The samples are complete `main.go` files. A few read a file from the current directory; the YAML they read is shown with them. ## Read YAML into a struct `oneyaml.Unmarshal` fills a struct by its `yaml` tags, the way `encoding/json` does with `json` tags. ```go package main import ( "fmt" "log" oneyaml "oneyaml.dev/go" ) type Config struct { Name string `yaml:"name"` Replicas int `yaml:"replicas"` Ports []int `yaml:"ports"` Debug bool `yaml:"debug"` } func main() { src := []byte(`name: api replicas: 2 ports: [80, 443] debug: false `) var cfg Config if err := oneyaml.Unmarshal(src, &cfg); err != nil { log.Fatal(err) } fmt.Printf("%+v\n", cfg) } ``` ```text {Name:api Replicas:2 Ports:[80 443] Debug:false} ``` YAML 1.2 is the default: plain `yes` is a string, plain `0755` is the integer 755, and quoted `"0755"` is a string. A value that doesn’t fit its field is a `*TypeError` with a path, a line and a column, and the rest of the document is still read (see [errors with line and column](https://recursiveai.org/oneyaml/go/cookbook/errors-with-line-and-column/)). ## Write YAML `oneyaml.Marshal` writes any Go value as block-style YAML. ```go package main import ( "fmt" "log" oneyaml "oneyaml.dev/go" ) type Config struct { Name string `yaml:"name"` Replicas int `yaml:"replicas"` Ports []int `yaml:"ports"` Labels map[string]string `yaml:"labels,omitempty"` } func main() { cfg := Config{ Name: "api", Replicas: 3, Ports: []int{80, 443}, Labels: map[string]string{"tier": "web"}, } out, err := oneyaml.Marshal(cfg) if err != nil { log.Fatal(err) } fmt.Print(string(out)) } ``` ```text name: api replicas: 3 ports: - 80 - 443 labels: tier: web ``` `NewEncoder` writes to an `io.Writer`, one document per `Encode`, and `Indent(n)` sets the indent. ## Edit a file and keep its comments To change a file that people also edit by hand, parse it into a `Document` instead. `Set` rewrites one value in its own style; comments and formatting elsewhere are kept byte for byte. ```go package main import ( "fmt" "log" oneyaml "oneyaml.dev/go" ) func main() { src := []byte(`# Deploy settings name: api replicas: 2 # raised by the release job ports: [80] `) doc, err := oneyaml.Parse(src) if err != nil { log.Fatal(err) } // Read a value by JSON Pointer. var replicas int if err := doc.Get("/replicas", &replicas); err != nil { log.Fatal(err) } fmt.Println("replicas =", replicas) // Change a value, append to a list, add a key. for _, step := range []struct { ptr string v any }{ {"/replicas", 3}, {"/ports/-", 443}, {"/region", "us-east-1"}, } { if err := doc.Set(step.ptr, step.v); err != nil { log.Fatal(err) } } fmt.Print(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. A value that is replaced keeps its style when it can hold the new value; a new member copies the style of its siblings, so the flow list `[80]` stays a flow list. `Remove`, `Rename`, `SetHeadComment` and `SetLineComment` do the rest ([Editing](https://recursiveai.org/oneyaml/go/docs/editing/)). ## Apply a JSON Patch or a merge patch A `Document` applies RFC 6902 JSON Patch and RFC 7396 merge patches. A patch applies atomically and keeps the file’s layout. ```go package main import ( "fmt" "log" oneyaml "oneyaml.dev/go" ) const src = `# Web service image: web:1.0 # pinned env: DEBUG: "1" LOG: info ports: [80] ` func main() { // JSON Patch (RFC 6902): all or nothing. doc, err := oneyaml.Parse([]byte(src)) if err != nil { log.Fatal(err) } err = doc.ApplyPatch([]byte(`[ {"op": "replace", "path": "/image", "value": "web:1.1"}, {"op": "add", "path": "/ports/-", "value": 443} ]`)) if err != nil { log.Fatal(err) } fmt.Print(doc) fmt.Println("---") // Merge patch (RFC 7396): null removes a key. doc, err = oneyaml.Parse([]byte(src)) if err != nil { log.Fatal(err) } merge := []byte(`{"env": {"DEBUG": null, "LOG": "warn"}}`) if err := doc.ApplyMergePatch(merge); err != nil { log.Fatal(err) } fmt.Print(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 YAML’s data model: `test` compares YAML values, so the integer `1` and the float `1.0` differ, and a write through an alias is refused. `Apply` takes the same operations built in Go with `PatchTest`, `PatchReplace` and the others ([Patches](https://recursiveai.org/oneyaml/go/docs/patches/)). ## Where next Recipes, each a complete program: - [Parse YAML into Go structs](https://recursiveai.org/oneyaml/go/cookbook/parse-yaml-into-structs/) and [read YAML without a struct](https://recursiveai.org/oneyaml/go/cookbook/read-yaml-without-a-struct/) - [Edit YAML and keep its comments](https://recursiveai.org/oneyaml/go/cookbook/edit-yaml-keep-comments/) and [merge or patch YAML](https://recursiveai.org/oneyaml/go/cookbook/patch-yaml/) - [Convert YAML to JSON and back](https://recursiveai.org/oneyaml/go/cookbook/yaml-to-json/) and [read Kubernetes manifests and multi-document files](https://recursiveai.org/oneyaml/go/cookbook/kubernetes-multi-document/) - [Report errors with line and column](https://recursiveai.org/oneyaml/go/cookbook/errors-with-line-and-column/), [anchors, aliases and merge keys](https://recursiveai.org/oneyaml/go/cookbook/anchors-aliases-merge-keys/) - [Read untrusted YAML safely](https://recursiveai.org/oneyaml/go/cookbook/untrusted-input/) and [switch from yaml.v3 with one import](https://recursiveai.org/oneyaml/go/cookbook/switch-from-yaml-v3/) The docs: - [Editing](https://recursiveai.org/oneyaml/go/docs/editing/), [patches](https://recursiveai.org/oneyaml/go/docs/patches/) and [limits](https://recursiveai.org/oneyaml/go/docs/limits/). The API reference with a runnable example for every feature is on [pkg.go.dev](https://pkg.go.dev/oneyaml.dev/go@v1.0.0). - Moving from another library: [yaml.v3](https://recursiveai.org/oneyaml/go/docs/migrate-yaml-v3/) and [sigs.k8s.io/yaml](https://recursiveai.org/oneyaml/go/docs/migrate-sigs-k8s-yaml/). - Choosing a library: [how it compares](https://recursiveai.org/oneyaml/go/docs/comparison/), [performance](https://recursiveai.org/oneyaml/go/docs/performance/) and [conformance](https://recursiveai.org/oneyaml/go/docs/conformance/). - What changed and what stays stable: the [changelog](https://recursiveai.org/oneyaml/go/docs/changelog/) and [stability](https://recursiveai.org/oneyaml/go/docs/overview/#stability). Choosing a Go YAML library, with four libraries compared on the same file: [Golang YAML](https://recursiveai.org/oneyaml/articles/golang-yaml/). # oneyaml-go **Pure-Go YAML 1.2 that edits a file and keeps its comments.** [![Go Reference](https://pkg.go.dev/badge/oneyaml.dev/go.svg)](https://pkg.go.dev/oneyaml.dev/go) [![CI](https://github.com/recursive-ai-oss/oneyaml-go/actions/workflows/ci.yml/badge.svg)](https://github.com/recursive-ai-oss/oneyaml-go/actions/workflows/ci.yml) [![Go version](https://img.shields.io/badge/go-1.24%2B-00ADD8)](#stability) [![License](https://img.shields.io/badge/license-MIT%20OR%20Apache--2.0-blue)](#license) YAML 1.2 with the Core schema by default; the conformance results and the known deviations are under [Correctness](#correctness). Edits change only the value you set, in its own style; comments and formatting elsewhere are kept byte for byte. Nesting, alias expansion, number size and patch work are bounded by default. Time per task for oneyaml-go and for the strongest Go competitor whose answer passed the benchmark's check, on the real-world files of each task. Each row says how many times faster oneyaml-go was: the competitor's time divided by ours, so higher is better. The method line under the table names the library commit that was measured, the date of the run and the machine it ran on; [About this run](https://recursiveai.org/oneyaml/go/docs/performance/#about-this-run) says which release that commit is. The figures are measured, not bounds, and later code may differ. Bar chart of the table below: how many times faster oneyaml-go is than the strongest competitor on each task, log scale, higher is better. 7 tasks: 7 better, 0 level, 0 mixed, 0 worse, 0 not comparable. | Task | `oneyaml-go` time (geometric mean) | Strongest competitor | Its time (geometric mean) | Speed against it (its time ÷ ours; higher is better) | Verdict | |---|---|---|---|---|---| | typed load | 121 µs | `yaml-v2 v2.4.0` | 234 µs | 1.94× faster | ▲ win | | generic load | 48.1 µs | `yaml-v2 v2.4.0` (6 of 7), `yaml-v3 v3.0.5` (1 of 7) | 135 µs | 2.79× faster | ▲ win | | dump | 9.5 µs | `yaml-v2 v2.4.0` (5 of 6), `yaml-v4 v4.0.0-rc.6` (1 of 6) | 114 µs | 12.0× faster | ▲ win | | document parse | 86.3 µs | `yaml-v3 v3.0.5` (2 of 3), `yaml-v4 v4.0.0-rc.6` (1 of 3) | 415 µs | 4.81× faster | ▲ win | | unchanged emit (write-back of an unedited document; not a rendering speed) | 4.31 ns | `goccy-go-yaml v1.19.2` | 186 µs | 43,100× faster | ▲ win | | edit script | 13.7 µs | `yaml-v3 v3.0.5` | 284 µs | 20.7× faster | ▲ win | | patch (RFC 6902) | 15.7 µs † (text API) | `json-patch v5.9.11` † (json-patch: text API, value-only) | 92.1 µs | 5.87× faster | ▲ win | Not in a row's mean: document parse: no competitor answer: `compose-wordpress`, `gha-workflow`, `openapi-api-with-examples`, `prometheus-config`; unchanged emit (write-back of an unedited document; not a rendering speed): no competitor answer: `compose-wordpress`, `gha-workflow`, `openapi-api-with-examples`, `prometheus-config`; edit script: no competitor answer: `compose-wordpress`, `gha-workflow`, `openapi-api-with-examples`, `prometheus-config`; patch (RFC 6902): no competitor answer: `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. Memory and the count of better, level and worse workloads in each row are on the full page. Method: **full run** of 2026-10-16T21:48:45Z. Kit revision 0abba1d8b4f2. `oneyaml-go eea67bb6b962`; competitors `yaml-v3 v3.0.5`, `yaml-v4 v4.0.0-rc.6`, `yaml-v2 v2.4.0`, `sigs-yaml v1.6.0`, `goccy-go-yaml v1.19.2`, `palantir-yamlpatch v1.5.0`, `palantir-yamlpatch-goccy v1.5.0`, `json-patch v5.9.11`. Runtime go1.26.0 (target linux/amd64), measured with testing.B: ns/op from testing.Benchmark at benchtime 1s. Memory: B/op and allocs/op from testing.B (ReportAllocs). 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 2.67 before, 4.06 after, at most 11.23 during). Every workload, the memory figures, what the unchanged-emit row does and does not measure, and how to reproduce the run: [Performance](https://recursiveai.org/oneyaml/go/docs/performance/). - **Measured against the other Go YAML libraries.** Typed and generic loads, dumps, document parsing, writing an unedited document, edits and patches are each timed against the strongest competitor whose answer passed the benchmark's check. The table above gives the verdict for each task, and [Performance](https://recursiveai.org/oneyaml/go/docs/performance/) has every workload with its time and memory. Rows where no competitor answered correctly have no comparison and are [not counted as wins](https://recursiveai.org/oneyaml/go/docs/performance/#rows-with-no-rival). - **YAML 1.2 Core schema by default.** Plain scalars follow the Core schema, so plain `yes` is a string, plain `0755` is the integer 755, and quoted `"0755"` is a string. YAML 1.1 forms come only from the [compat packages](#compatibility-adapters-for-yamlv3-and-sigsk8sioyaml). Results against the yaml-test-suite, and the known deviations, are under [Correctness](#correctness). - **Minimal-diff edits.** `Document.Set` rewrites one value in its own style and falls back to the nearest style that can hold the new value. Reading it back with this package always gives exactly the value you set. Plainness follows the Core schema, so a new plain `yes` is a string here but a boolean to a YAML 1.1 reader; pass [`YAML11Strings()`](https://recursiveai.org/oneyaml/go/docs/editing/#when-the-style-cannot-hold-the-value) when such programs also read the file. `Rename` changes a key in place, and `SetHeadComment` and `SetLineComment` write comments. The benchmark checks every edit byte for byte [before it times it](https://recursiveai.org/oneyaml/go/docs/performance/#the-correctness-check-before-timing). See [Editing](https://recursiveai.org/oneyaml/go/docs/editing/). - **Patches.** RFC 6902 JSON Patch operations, RFC 6901 pointers and RFC 7396 merge patches, applied to a YAML file with YAML's data model: `test` compares YAML values, so the integer `1` and the float `1.0` [differ](https://recursiveai.org/oneyaml/go/docs/patches/#yaml-rules-at-a-glance) where RFC 6902 would call them equal, and a write through an alias is refused. A patch applies atomically and keeps the file's layout. See [Patches](https://recursiveai.org/oneyaml/go/docs/patches/). - **Bounded by default.** Nesting depth, alias expansion, the digits of a number and the work of a patch are bounded unless you pass `NoLimits()`. Input size, documents per stream and scalar length are bounded only with `Untrusted()`; select it, and size it with `Limits`, for input you do not control. Your own hooks run outside these bounds, and the compat packages have no `Untrusted()` mode. A panic caused by input is treated as a [security bug](https://github.com/recursive-ai-oss/oneyaml-go/blob/7a4c8e39f041b945ee5f121c055f03bc2d7d01d9/SECURITY.md). No `unsafe` and no cgo. See [Limits](https://recursiveai.org/oneyaml/go/docs/limits/). - **Compatibility adapters for yaml.v3 and sigs.k8s.io/yaml.** For the common API listed in each migration guide, one import change keeps your call sites; the [known differences](#compatibility-adapters-for-yamlv3-and-sigsk8sioyaml) are listed. See [Migrating from yaml.v3](https://recursiveai.org/oneyaml/go/docs/migrate-yaml-v3/) and [from sigs.k8s.io/yaml](https://recursiveai.org/oneyaml/go/docs/migrate-sigs-k8s-yaml/). - **No dependencies** outside the standard library. ## Install ```sh go get oneyaml.dev/go@latest ``` ## Quick start Change one value in a file. The comment, the block scalar and every other byte stay as they were. (The examples import `fmt`, `log` and `oneyaml "oneyaml.dev/go"`.) ```go src := []byte(`# deploy settings name: api replicas: 2 # raised by the release job script: | make build `) doc, err := oneyaml.Parse(src) if err != nil { log.Fatal(err) } if err := doc.Set("/replicas", 3); err != nil { log.Fatal(err) } if err := doc.Set("/script", "make build\nmake test\n"); err != nil { log.Fatal(err) } fmt.Print(string(doc.Bytes())) // Output: // # deploy settings // name: api // replicas: 3 # raised by the release job // script: | // make build // make test ``` Reading into a struct works as it does in `encoding/json`: ```go src := []byte(`# deploy settings name: api replicas: 2 ports: [80, 443] labels: tier: web `) var cfg struct { Name string `yaml:"name"` Replicas int `yaml:"replicas"` Ports []int `yaml:"ports"` Labels map[string]string `yaml:"labels"` } if err := oneyaml.Unmarshal(src, &cfg); err != nil { log.Fatal(err) } fmt.Println(cfg.Name, cfg.Replicas, cfg.Ports, cfg.Labels) // Output: api 2 [80 443] map[tier:web] ``` The package documentation on [pkg.go.dev](https://pkg.go.dev/oneyaml.dev/go) has a runnable example for every feature. ## When to use it, and when not to Use it when you need to change a YAML file that people also edit by hand, apply a JSON Patch to one, read YAML 1.2 with the Core schema, or read input you do not control with `Untrusted()`. Think twice when: - You need comments in any place. The comment setters write a comment above an entry or after a value, and refuse places such as most of the inside of a flow collection ([Comments](https://recursiveai.org/oneyaml/go/docs/editing/#comments)). - You need to stream events. The parser is not exposed; `Parse` and `NewDecoder` read a whole document. - You hold very large files in memory. A `Document` keeps the text and an index of it, which is an order of magnitude larger than the text for dense input. Set `MaxInputBytes`, or use `Untrusted()`, for input from outside. - Your data uses a collection as a mapping key and you read it into `any`. A Go map cannot hold it; a `Document` can. - Your program depends on go-yaml quirks. The [compat packages](#compatibility-adapters-for-yamlv3-and-sigsk8sioyaml) reproduce documented behaviour, not bugs, and cover yaml.v3 and sigs.k8s.io/yaml only. [Comparison](https://recursiveai.org/oneyaml/go/docs/comparison/) says where other libraries are the better choice. ## Correctness Conformance is measured with [oneyaml-kit](https://github.com/recursive-ai-oss/oneyaml-kit), the conformance kit shared by the oneyaml libraries, against the yaml-test-suite release `data-2022-01-17`. The rows below are generated. [Conformance](https://recursiveai.org/oneyaml/go/docs/conformance/) has the whole report, explains what each column means, including the strict column for invalid input, and lists every known deviation. Method: kit 0.1.0 (corpus digest `b1c5e8ce5ca5`, kit revision `0abba1d8b4f2`), every case including the slow ones, run 2026-10-16, library revision `eea67bb6b962`, yaml-test-suite `data-2022-01-17` (revision `6e6c296a`) plus 605 kit cases. Generated by `bin/kit run --slow --adapter oneyaml-go` from the kit's result file `oneyaml-go.json`; do not edit. | Group | Cases | Loose | Strict | Strict, corrected | Unsupported | Strict only | Emulated | |---|---:|---:|---:|---:|---:|---:|---:| | yaml-test-suite · events · invalid input | 94 | 94/94 | 0/94 | 0/94 | 0 | 94 | 0 | | yaml-test-suite · events · valid input | 308 | 308/308 | 308/308 | – | 0 | 0 | 0 | | yaml-test-suite · values (in.json) · valid input | 279 | 278/279 | 278/279 | 279/279 | 0 | 0 | 0 | | **yaml-test-suite · events · all 402** | 402 | 402/402 | 308/402 | 308/402 | 0 | 94 | 0 | The library is fuzzed with Go's native fuzzing. The fuzz targets cover [`Unmarshal`](https://github.com/recursive-ai-oss/oneyaml-go/blob/7a4c8e39f041b945ee5f121c055f03bc2d7d01d9/unmarshal_fuzz_test.go), [`Document` reads](https://github.com/recursive-ai-oss/oneyaml-go/blob/7a4c8e39f041b945ee5f121c055f03bc2d7d01d9/document_decode_fuzz_test.go), [edits](https://github.com/recursive-ai-oss/oneyaml-go/blob/7a4c8e39f041b945ee5f121c055f03bc2d7d01d9/document_prop_test.go) and [patches](https://github.com/recursive-ai-oss/oneyaml-go/blob/7a4c8e39f041b945ee5f121c055f03bc2d7d01d9/patch_prop_test.go), and the compat packages [`compat/yamlv3`](https://github.com/recursive-ai-oss/oneyaml-go/blob/7a4c8e39f041b945ee5f121c055f03bc2d7d01d9/compat/yamlv3/fuzz_test.go) and [`compat/k8syaml`](https://github.com/recursive-ai-oss/oneyaml-go/blob/7a4c8e39f041b945ee5f121c055f03bc2d7d01d9/compat/k8syaml/fuzz_test.go). The patch and compat packages are also checked against their targets by differential tests. ## Compatibility adapters for yaml.v3 and sigs.k8s.io/yaml The compat packages are adapters for the common API of their targets, listed in each migration guide. If your code uses that API, changing the import path keeps your call sites; the package name stays `yaml`. ```sh # gopkg.in/yaml.v3 -> oneyaml.dev/go/compat/yamlv3 # sigs.k8s.io/yaml -> oneyaml.dev/go/compat/k8syaml ``` Swapping your import does not change the code of your dependencies. A type from another module whose `UnmarshalYAML` takes gopkg.in/yaml.v3's `*yaml.Node` cannot be called by `compat/yamlv3`, so reading into it returns an error that names the type. (yaml.v3 itself would skip such a method and fill the type field by field, without what the method checks.) Read that part through a wrapper type in your own code whose hook the adapter can call, or decode that part with yaml.v3. [Hooks you do not own](https://recursiveai.org/oneyaml/go/docs/migrate-yaml-v3/#check-after-the-swap) has the details. They read YAML 1.1 forms the way their targets do, but they are not identical. Known gaps and differences: - **yaml.v3 comments:** comments land on the same nodes as in yaml.v3 in the common layouts, and are written back where yaml.v3 writes them, inside flow collections too. One flow-mapping layout puts a comment on a different node, and where yaml.v3 writes text that does not read back, the adapter writes text that does. - **yaml.v3 hooks:** an `UnmarshalYAML` or `MarshalYAML` method of a form the adapter cannot call is an error, where yaml.v3 skips it. That includes a `MarshalYAML` that returns gopkg.in/yaml.v3's own `*yaml.Node` (or a `yaml.Node` of that module anywhere in the value): the error names the type; build the adapter's `yaml.Node` instead. - **sigs.k8s.io/yaml:** `JSONObjectToYAMLObject` and the subpackages of sigs.k8s.io/yaml are not provided. - **Numbers:** in `compat/yamlv3`, a plain `3.5` read into an integer field is a type error instead of being truncated. In `compat/k8syaml`, a float read into a string field keeps its full precision, where sigs.k8s.io/yaml rounds it to 32 bits. - **Valid and invalid input:** both read valid YAML that their targets refuse, `compat/yamlv3` refuses invalid YAML that yaml.v3 accepts, and both return errors where their targets panic. - **Limits:** both always run with the default limits and take no options, so there is no `Untrusted()` mode. Bound the input size yourself ([how](https://recursiveai.org/oneyaml/go/docs/limits/#loading-a-configuration-file-strictly)). Each package's documentation lists every difference ([yamlv3](https://pkg.go.dev/oneyaml.dev/go/compat/yamlv3), [k8syaml](https://pkg.go.dev/oneyaml.dev/go/compat/k8syaml)), and the migration guides have a checklist. New code should use the `oneyaml` package. ## Documentation | You want to | Read | |---|---| | Look up a function or type | [pkg.go.dev](https://pkg.go.dev/oneyaml.dev/go) | | Change a value, rename a key or write a comment, and keep the file's style | [Editing](https://recursiveai.org/oneyaml/go/docs/editing/) | | Apply a JSON Patch or merge patch | [Patches](https://recursiveai.org/oneyaml/go/docs/patches/) | | Read input you do not control | [Limits](https://recursiveai.org/oneyaml/go/docs/limits/), [Security](https://github.com/recursive-ai-oss/oneyaml-go/blob/7a4c8e39f041b945ee5f121c055f03bc2d7d01d9/SECURITY.md) | | Load a configuration file strictly | [Loading a configuration file strictly](https://recursiveai.org/oneyaml/go/docs/limits/#loading-a-configuration-file-strictly) | | Know what is and is not conformant | [Conformance](https://recursiveai.org/oneyaml/go/docs/conformance/) | | Move from yaml.v3 | [Migrating from yaml.v3](https://recursiveai.org/oneyaml/go/docs/migrate-yaml-v3/) | | Move from sigs.k8s.io/yaml | [Migrating from sigs.k8s.io/yaml](https://recursiveai.org/oneyaml/go/docs/migrate-sigs-k8s-yaml/) | | See speed and memory against other Go libraries | [Performance](https://recursiveai.org/oneyaml/go/docs/performance/) | | Compare with other Go libraries | [Comparison](https://recursiveai.org/oneyaml/go/docs/comparison/) | | See what changed | [Changelog](https://recursiveai.org/oneyaml/go/docs/changelog/) | | Know what is stable and which Go versions are supported | [Stability](#stability) | | Contribute | [Contributing](https://github.com/recursive-ai-oss/oneyaml-go/blob/7a4c8e39f041b945ee5f121c055f03bc2d7d01d9/CONTRIBUTING.md) | ## Stability From 1.0.0 the module follows Go's [rules for a stable module](https://go.dev/doc/modules/version-numbers), in the spirit of the [Go 1 compatibility promise](https://go.dev/doc/go1compat). Within major version 1: - **Your code keeps compiling and keeps its meaning.** Code that uses the exported API of `oneyaml.dev/go`, `oneyaml.dev/go/compat/yamlv3` or `oneyaml.dev/go/compat/k8syaml` builds with every later 1.x release and behaves as documented. That covers the error types, the sentinel errors you match with `errors.Is` and `errors.As`, and the `Limit` and `Category` constants. - **Written bytes stay the same.** The bytes that `Marshal` and an `Encoder` write with default options do not change, and neither do the bytes that an edit leaves untouched. A minor release may still make these changes: - **Additions:** new names, methods, struct fields and `Limit` constants. A new field breaks a struct literal without field names, so write `LimitSet`, the error types and the other structs of these packages with field names, as in `oneyaml.LimitSet{MaxDepth: 50}`, and give a `switch` on `Limit` a default case. - **Bug fixes**, including refusing input that the YAML 1.2 specification calls invalid, and writing text that reads back as the value written where the output did not. Each such fix is listed in the [changelog](https://recursiveai.org/oneyaml/go/docs/changelog/). - **Error messages:** their text is written for people and may change. Match errors by type, sentinel and constant. - **The compat packages:** where `compat/yamlv3` or `compat/k8syaml` places comments or lays out output differently from yaml.v3 or sigs.k8s.io/yaml, a minor release may bring it closer to its target. Patch releases only fix bugs. Security fixes go into the latest minor release ([security policy](https://github.com/recursive-ai-oss/oneyaml-go/blob/7a4c8e39f041b945ee5f121c055f03bc2d7d01d9/SECURITY.md)). Any other change that breaks code needs a new major version with its own module path, `oneyaml.dev/go/v2`, so code that imports `oneyaml.dev/go` keeps building against 1.x. Releases before 1.0 could break code in a minor release. The [changelog](https://recursiveai.org/oneyaml/go/docs/changelog/) marks each such change, with what to write instead. **Go versions.** The minimum is Go 1.24 (`go 1.24` in `go.mod`), and CI tests Go 1.24, 1.25, 1.26 and 1.27. Raising the minimum takes a minor release and a note in the changelog. Go itself supports only its two newest major releases ([release policy](https://go.dev/doc/devel/release#policy)), so the minimum reaches back past the releases that still get fixes from Go. This is the only place this policy is stated; other pages link here. ## License Licensed under either of [Apache License, Version 2.0](https://github.com/recursive-ai-oss/oneyaml-go/tree/7a4c8e39f041b945ee5f121c055f03bc2d7d01d9/LICENSE-APACHE) or [MIT license](https://github.com/recursive-ai-oss/oneyaml-go/tree/7a4c8e39f041b945ee5f121c055f03bc2d7d01d9/LICENSE-MIT), at your option. Unless you state otherwise, any contribution you submit for inclusion is dual licensed as above, without additional terms or conditions. # Editing How `Document` changes a YAML file and what it promises to leave alone. For the API reference see [pkg.go.dev](https://pkg.go.dev/oneyaml.dev/go#Document); for patches see [Patches](https://recursiveai.org/oneyaml/go/docs/patches/). `Parse` returns a `Document` that holds the whole input. Until you edit it, `Bytes` returns exactly the bytes you gave it, including comments, blank lines, quoting, indentation and line endings. An edit changes only the value you set, in its own style; comments and formatting elsewhere are kept byte for byte. The same holds for the edits that change a key ([Renaming a key](#renaming-a-key)) or a comment ([Comments](#comments)). `fmt.Print(doc)` prints the document as it stands, because `Document` and `Node` have a `String` method. Two rules about what you share with a `Document`: - **One goroutine at a time.** A `Document` is not safe for concurrent use, reads included: a read can finish internal work that an earlier edit left for later. Guard it with a mutex, or give each goroutine its own `Document`. - **`Bytes` is read-only.** `Parse` copies its input, so you may reuse the slice you passed it. The slice `Bytes` returns belongs to the document and must not be modified. Edits build new text rather than changing it, so the slice stays valid after later edits; to change the bytes, copy them first with `bytes.Clone`. You address a value with an [RFC 6901](https://www.rfc-editor.org/rfc/rfc6901) pointer: `/replicas`, `/tags/0`, `/tags/-` for the end of a sequence. A pointer token matches a mapping key by its text, so `/1` finds the key `1`. In a pointer, `~` is written `~0` and `/` is written `~1`, so the key `app.kubernetes.io/name` is the token `app.kubernetes.io~1name`. When a key comes from data, build the pointer with `oneyaml.Pointer` rather than with `"/" + key`, so that each token is escaped. `Node.Pointer` returns the same form for a node you have read. ```go doc, err := oneyaml.Parse([]byte("metadata:\n labels:\n app.kubernetes.io/name: web\n")) if err != nil { log.Fatal(err) } ptr := oneyaml.Pointer("metadata", "labels", "app.kubernetes.io/name") fmt.Println(ptr) if err := doc.Set(ptr, "api"); err != nil { log.Fatal(err) } fmt.Print(doc) // Output: // /metadata/labels/app.kubernetes.io~1name // metadata: // labels: // app.kubernetes.io/name: api ``` ## Nodes after an edit `Document.Root`, `Node.Find` and `Document.Get` give you a `Node`, a read-only view of the document as it was when you read it. An edit makes every `Node` read before it stale, so read, edit, then look the node up again. Use `Find`, which returns an error saying why there is no value, where a failure matters; `Lookup` reports every failure as `false`. ```go doc, err := oneyaml.Parse([]byte("spec:\n replicas: 2 # set by CI\n")) if err != nil { log.Fatal(err) } spec, err := doc.Root().Find("spec") if err != nil { log.Fatal(err) } replicas, err := spec.Find("replicas") if err != nil { log.Fatal(err) } fmt.Println(replicas.Value()) if err := doc.Set("/spec/replicas", 3); err != nil { log.Fatal(err) } // spec was read before the edit, so it is stale. _, err = spec.Find("replicas") fmt.Println(errors.Is(err, oneyaml.ErrStaleNode)) // Look it up again from the root. if spec, err = doc.Root().Find("spec"); err != nil { log.Fatal(err) } if replicas, err = spec.Find("replicas"); err != nil { log.Fatal(err) } fmt.Println(replicas.Value(), replicas.LineComment()) // Output: // 2 // true // 3 # set by CI ``` [Reading while you edit](#reading-while-you-edit) has the rest: how to tell a stale node from an empty value, loops that edit as they go, and collecting pointers first. ## Two block-scalar cases Two cases illustrate the rules. In the first, the new value ends in a line break, so the block literal keeps its style and only its text changes: ```go doc, err := oneyaml.Parse([]byte(`#hello a: 10 #abc 123 b: | this is a test c: 13 `)) if err != nil { log.Fatal(err) } if err := doc.Set("/b", "this is a test2\n"); err != nil { log.Fatal(err) } fmt.Print(string(doc.Bytes())) // Output: // #hello // a: 10 // // #abc 123 // b: | // this is a test2 // c: 13 ``` In the second, the new value has no final line break. Reading the document back must give exactly that value, so the header changes from `|` to `|-` and nothing else does: ```go doc, err := oneyaml.Parse([]byte(`#hello a: 10 #abc 123 b: | this is a test c: 13 `)) if err != nil { log.Fatal(err) } if err := doc.Set("/b", "this is a test2"); err != nil { log.Fatal(err) } fmt.Print(string(doc.Bytes())) var b string if err := doc.Get("/b", &b); err != nil { log.Fatal(err) } fmt.Printf("%q\n", b) // Output: // #hello // a: 10 // // #abc 123 // b: |- // this is a test2 // c: 13 // "this is a test2" ``` Setters are exact: whatever you set, `Get` returns, because this package reads the file with the YAML 1.2 Core schema. Programs that read YAML with YAML 1.1 rules, such as Kubernetes tooling built on sigs.k8s.io/yaml and code on yaml.v2, may read a new value differently: to them a plain `yes` is a boolean and `0755` is octal. If such programs also read the file, parse it with `YAML11Strings()` ([below](#when-the-style-cannot-hold-the-value)). ## When the style cannot hold the value A value keeps its style when that style can hold the new value. When it cannot, only that value is rewritten, in the nearest style that can. Plainness is judged by the Core schema, because that is how this package reads the file: `yes` stays plain, `true` is quoted. A string with a line break that replaces an inline scalar is written double-quoted. ```go doc, err := oneyaml.Parse([]byte("a: plain\nb: plain\nc: plain\nd: \"quoted\"\n")) if err != nil { log.Fatal(err) } for _, edit := range []struct { ptr string v any }{ {"/a", "nginx:1.25"}, // Core reads this as a string: it stays plain {"/b", "true"}, // Core reads this as a boolean: it is quoted {"/c", "two\nlines"}, // a line break needs an escape: double quotes {"/d", "now: plain?"}, // a double-quoted scalar stays double-quoted } { if err := doc.Set(edit.ptr, edit.v); err != nil { log.Fatal(err) } } fmt.Print(string(doc.Bytes())) // Output: // a: nginx:1.25 // b: 'true' // c: "two\nlines" // d: "now: plain?" ``` A YAML 1.1 reader takes a plain `yes`, `on`, `0755` or `12:30` for a boolean, a number or a time. If the file is also read by such programs (Kubernetes tooling built on sigs.k8s.io/yaml, or code on yaml.v2), pass `YAML11Strings()` to `Parse`. New strings written by `Set`, patches and merge patches, and keys written by `Rename`, then follow the stricter rule that `Marshal` always uses: a string stays plain only when YAML 1.1 readers read the same string too. An existing quoted or block style is kept, and values you do not edit are not touched. ```go src := []byte("debug: false\nname: x\n") // By default the Core schema decides: "yes" is a string there, so it stays plain. doc, err := oneyaml.Parse(src) if err != nil { log.Fatal(err) } if err := doc.Set("/debug", "yes"); err != nil { log.Fatal(err) } fmt.Print(doc) // With YAML11Strings, a string that YAML 1.1 reads as a boolean is quoted. doc, err = oneyaml.Parse(src, oneyaml.YAML11Strings()) if err != nil { log.Fatal(err) } if err := doc.Set("/debug", "yes"); err != nil { log.Fatal(err) } if err := doc.Set("/name", "hello"); err != nil { log.Fatal(err) } fmt.Print(doc) // Output: // debug: yes // name: x // debug: 'yes' // name: hello ``` ## Adding and removing `Set` on a key that does not exist adds it at the end of its mapping, in the style of its neighbours: the same quoting, the same indentation, block or flow. `Set` at `/-`, or at the index equal to the length, appends to a sequence. A null that is already in the file keeps its spelling. ```go doc, err := oneyaml.Parse([]byte("# ports\nname: 'api'\nport: 80\nnote: ~\n")) if err != nil { log.Fatal(err) } if err := doc.Set("/owner", "ops"); err != nil { log.Fatal(err) } if err := doc.Set("/note", nil); err != nil { log.Fatal(err) } if err := doc.Set("/port", 8080); err != nil { log.Fatal(err) } fmt.Print(string(doc.Bytes())) // Output: // # ports // name: 'api' // port: 8080 // note: ~ // owner: 'ops' ``` `Remove` deletes an entry together with the comment on its line, the comment directly above it and any comment inside its value. A comment above the next entry stays. ```go doc, err := oneyaml.Parse([]byte("a: 1\n# about b\nb: 2 # two\n# about c\nc: 3\n")) if err != nil { log.Fatal(err) } if err := doc.Remove("/b"); err != nil { log.Fatal(err) } fmt.Print(string(doc.Bytes())) // Output: // a: 1 // # about c // c: 3 ``` Removing the root is refused. ## Renaming a key `Rename(ptr, newKey)` changes the key of the entry at `ptr` where it stands. The value, the entry's place in its mapping, its comments, and an anchor or tag on the key stay as they are. The new key keeps the old key's style (plain, single- or double-quoted) when that style can hold it. Otherwise it gets the nearest style that can: single quotes for a plain key that needs quoting (and, with `YAML11Strings()`, for one that YAML 1.1 readers would take for something else, so renaming to `on` writes `'on'`), double quotes for one with a line break or an unprintable character. The new key is always a string, so renaming the key `1` to `"2"` writes `'2'`. ```go doc, err := oneyaml.Parse([]byte(`# chart values image: nginx replicas: 2 # scaled by the autoscaler port: 80 `)) if err != nil { log.Fatal(err) } if err := doc.Rename("/replicas", "replicaCount"); err != nil { log.Fatal(err) } fmt.Print(doc) err = doc.Rename("/port", "image") fmt.Println(errors.Is(err, oneyaml.ErrKeyExists)) // Output: // # chart values // image: nginx // replicaCount: 2 # scaled by the autoscaler // port: 80 // true ``` `Rename` refuses, and leaves the document unchanged, when: - the mapping already has the new key (`ErrKeyExists`); - the pointer is the root or an item of a sequence (`ErrNotAnEntry`); - the pointer leads through an alias (`ErrAliasWrite`). Renaming an entry whose value is an alias is allowed; - the key occurs twice in its mapping (`ErrAmbiguousKey`); - the key carries an anchor that an alias uses (`ErrAnchorInUse`), because the alias would read the new key too. This is the same rule as for `Remove`. An anchor on the value does not count; - the key is empty, a collection, an alias or a block scalar, or is the merge key `<<`; the new key is longer than 1000 bytes or is not valid UTF-8; or a tag on the key does not fit the new key (`ErrUnsupportedEdit`). Like `Set`, a rename parses again only the entries around the key where it can, rather than the whole document; inside a flow collection, for one, it parses the whole document again. To check that the new key is not already there, it uses an index of the mapping's keys, built the first time it is needed and kept up to date by later renames, so a run of renames through one wide mapping does not read every key again for each rename. ## Comments Comments outside the edited value are never touched. Comments inside a value you replace or remove go with it, because they described the old value. `Node.HeadComment` returns the comment lines directly above an entry or item, and `Node.LineComment` the comment after a value on its line. Both return trimmed text, as yaml.v3 does: each line starts with its `#`, without indentation or trailing blanks, and the lines are joined by line feeds. The line comment of an entry whose value is a block collection or empty is the one after its colon or dash. `Node.Source` still gives the exact text of a value. `SetHeadComment(ptr, text)` replaces the comment lines above the entry or item at `ptr`, and an empty text removes them. A line without a leading `#` gets `# ` in front, a blank line inside the text becomes a bare `#`, and the lines are written at the entry's indentation. The root pointer `""` sets the comment above the first content line of the document, below a `---` marker line. ```go doc, err := oneyaml.Parse([]byte(`service: # old note port: 80 host: example.org `)) if err != nil { log.Fatal(err) } if err := doc.SetHeadComment("/service/port", "the public port"); err != nil { log.Fatal(err) } if err := doc.SetHeadComment("/service/host", "first line\nsecond line"); err != nil { log.Fatal(err) } fmt.Print(doc) if err := doc.SetHeadComment("/service/host", ""); err != nil { log.Fatal(err) } fmt.Print(doc) // Output: // service: // # the public port // port: 80 // # first line // # second line // host: example.org // service: // # the public port // port: 80 // host: example.org ``` `SetLineComment(ptr, text)` replaces the comment after the value at `ptr`, and an empty text removes it. The comment goes after a scalar, after the closing bracket of a flow collection, after the header of a block scalar, or after the colon or dash of an entry whose value is a block collection or empty: ```go doc, err := oneyaml.Parse([]byte(`replicas: 2 script: | make build env: mode: prod `)) if err != nil { log.Fatal(err) } for ptr, text := range map[string]string{ "/replicas": "scaled by the autoscaler", "/script": "run by CI", "/env": "per environment", } { if err := doc.SetLineComment(ptr, text); err != nil { log.Fatal(err) } } fmt.Print(doc) replicas, _ := doc.Root().Lookup("replicas") fmt.Println(replicas.LineComment()) // Output: // replicas: 2 # scaled by the autoscaler // script: | # run by CI // make build // env: # per environment // mode: prod // # scaled by the autoscaler ``` Some places cannot hold a comment, and the setters refuse them with `ErrUnsupportedEdit`: - a head comment for anything inside a flow collection; - a line comment inside a flow collection, unless nothing, not even a comma, follows the item on its line, as for the last item of a flow collection written one item per line; - a head comment for an entry that shares its line with another, such as the first key of a mapping written after `- `; - a line comment for the root block collection, or for a value that something else follows on its line. Text that is not valid UTF-8, that holds a control character or a line separator other than the line feed, or, for a line comment, that holds a line break, is refused with `ErrInvalidComment`. In every case the document reads back with the same values, and only the comment lines change. Like `Set` and `Rename`, the comment setters parse again only the entries around the comment where they can, so a run of comment edits in a large document does not read the whole document for each one. Inside a flow collection, for one, they parse the whole document again. ## Anchors, aliases and tags An anchor stays on a replaced value, so every alias reads the new value. A tag stays when the new value reads back as exactly that value under it, and is dropped otherwise. Replacing an alias node itself writes the new value in its place. ```go doc, err := oneyaml.Parse([]byte("a: &x 1\nb: *x\nt: !custom foo\n")) if err != nil { log.Fatal(err) } if err := doc.Set("/a", 2); err != nil { log.Fatal(err) } if err := doc.Set("/t", "bar"); err != nil { log.Fatal(err) } fmt.Print(string(doc.Bytes())) var b int if err := doc.Get("/b", &b); err != nil { log.Fatal(err) } fmt.Println(b) if err := doc.Set("/b", 9); err != nil { log.Fatal(err) } fmt.Print(string(doc.Bytes())) // Output: // a: &x 2 // b: *x // t: !custom bar // 2 // a: &x 2 // b: 9 // t: !custom bar ``` Two edits are refused because they would change a value you did not address: - Writing below an alias (`/copy/x` when `copy` is `*b`) would change every place that shares the anchored node. Edit the anchored node instead. ```go doc, err := oneyaml.Parse([]byte("base: &b\n x: 1\ncopy: *b\n")) if err != nil { log.Fatal(err) } err = doc.Set("/copy/x", 2) fmt.Println(errors.Is(err, oneyaml.ErrAliasWrite)) err = doc.Set("/base/x", 2) fmt.Println(err) fmt.Print(string(doc.Bytes())) // Output: // true // // base: &b // x: 2 // copy: *b ``` - Removing an anchored node that an alias still uses would leave the alias without a target. ```go doc, err := oneyaml.Parse([]byte("base: &b\n x: 1\ncopy: *b\n")) if err != nil { log.Fatal(err) } err = doc.Remove("/base") fmt.Println(errors.Is(err, oneyaml.ErrAnchorInUse)) fmt.Print(string(doc.Bytes())) // Output: // true // base: &b // x: 1 // copy: *b ``` ## Merge keys With `MergeKeys()`, a pointer also finds the keys that a `<<` merge brings into a mapping, with the precedence decoding gives them: a key the mapping has itself wins, and an earlier merge source wins over a later one. `Get`, the patch `test` operation, the `from` of a patch `copy`, `Node.Find` and `Node.Lookup` read such keys. `Entries`, `Items` and `Len` describe the mapping as written, and `Get` of the merge key itself (`/web/<<`) reads the merge source as written. A merged key belongs to its merge source, and changing it there would change every mapping that merges the source, as a write through an alias would. So: - `Set` on a key that only a merge source has adds an explicit key to the mapping, which overrides the merged one; - `Remove`, the patch operations `remove`, `replace` and `move`, and a merge patch member `null`, of such a key are refused with `ErrAliasWrite`, as is any write below it (`/web/image/tag`) or through the merge key (`/web/< ```go src := "base: &base {image: nginx, port: 80}\nweb:\n <<: *base\n port: 8080\n" doc, err := oneyaml.Parse([]byte(src), oneyaml.MergeKeys()) if err != nil { log.Fatal(err) } var image string var port int if err := doc.Get("/web/image", &image); err != nil { // from the merge log.Fatal(err) } if err := doc.Get("/web/port", &port); err != nil { // the mapping's own key wins log.Fatal(err) } fmt.Println(image, port) // Remove would have to write into the merge source, so it is refused. err = doc.Remove("/web/image") fmt.Println(errors.Is(err, oneyaml.ErrAliasWrite)) // Set adds a key of its own to web, which overrides the merged one. if err := doc.Set("/web/image", "caddy"); err != nil { log.Fatal(err) } fmt.Print(string(doc.Bytes())) // Output: // nginx 8080 // true // base: &base {image: nginx, port: 80} // web: // <<: *base // port: 8080 // image: caddy ``` Merge sources are searched depth-first, each mapping once, so a source that merges itself, directly or through others, does not loop. The keys a search reads count against `MaxAliasNodes` ([Limits](https://recursiveai.org/oneyaml/go/docs/limits/#each-limit)). A search past it fails with a `*LimitError`, whether it serves `Get`, `Node.Find`, a patch or an edit. `Node.Lookup` reports such a search as `false`, the same as an absent key, so use `Node.Find` when the difference matters, as before you fall back to a default. Adding a new key with `Set` does not search merge sources. ```go // With MergeKeys, a search through merge sources is bounded by MaxAliasNodes. // Find tells a refused search from an absent key; Lookup reports both as false. src := "base: &base {a: 1, b: 2, c: 3, d: 4}\nuse:\n <<: *base\n own: 5\n" limits := oneyaml.DefaultLimits() limits.MaxAliasNodes = 2 doc, err := oneyaml.Parse([]byte(src), oneyaml.MergeKeys(), oneyaml.Limits(limits)) if err != nil { log.Fatal(err) } var use oneyaml.Node if err := doc.Get("/use", &use); err != nil { log.Fatal(err) } own, err := use.Find("own") fmt.Println(own.Value(), err) // Neither "missing" nor "d" is a key of use itself, so both need the merge // search, which reads more keys than the bound allows. The answer is the // *LimitError, not ErrNotFound: whether the key exists is unknown. _, err = use.Find("missing") fmt.Println(errors.Is(err, oneyaml.ErrNotFound), errors.As(err, new(*oneyaml.LimitError))) _, err = use.Find("d") fmt.Println(errors.Is(err, oneyaml.ErrNotFound), errors.As(err, new(*oneyaml.LimitError))) // Output: // 5 // false true // false true ``` Without `MergeKeys()`, `<<` is an ordinary key, and pointers address only the keys as written. The edits, patches, `Node.Find` and `Node.Lookup` follow the option given to `Parse`; `Get` and `Decode` can also take `MergeKeys()` or `MergeKeys(false)` for one read ([Reading values back](#reading-values-back)). ## Errors An edit that cannot be made leaves the document unchanged and returns an `*EditError`. It names the operation and the pointer, and `errors.Is` matches its kind against `ErrNotFound`, `ErrAliasWrite`, `ErrAmbiguousKey`, `ErrAnchorInUse`, `ErrUnsupportedEdit`, `ErrKeyExists`, `ErrNotAnEntry`, `ErrInvalidComment` and the other sentinel errors. When the document was parsed with a bound on input size (`MaxInputBytes`, which `Untrusted()` sets), `Set`, `Rename`, `SetHeadComment`, `SetLineComment` and patches refuse an edit that would make the text longer than the bound, counted in the encoding the document is written in. `errors.As` finds the `*LimitError` inside the `*EditError` ([Limits](https://recursiveai.org/oneyaml/go/docs/limits/#each-limit)). An edit that makes the text shorter is always allowed. ```go doc, err := oneyaml.Parse([]byte("a: 1\n")) if err != nil { log.Fatal(err) } err = doc.Set("/a/b/c", 2) var ee *oneyaml.EditError if errors.As(err, &ee) { fmt.Println(ee.Op, ee.Pointer, errors.Is(err, oneyaml.ErrNotFound)) } // Output: // set /a/b/c true ``` ## Reading values back `Get(ptr, &v)` reads the value at a pointer into any target `Unmarshal` accepts, `Decode(&v)` reads the whole document, and `Node.Decode` reads the part below a node. Options given to one of these reads apply to that call only, on top of the options the document was parsed with, so one file can be read strictly in one place and loosely in another. `KnownFields`, `MergeKeys`, `Limits`, `Untrusted` and `NoLimits` take effect, also in the decodes an `UnmarshalYAML` hook makes during the read. A bound given to a read replaces the document's own for that read, looser as well as tighter, so `NoLimits()` on a document parsed with `Untrusted()` lifts its bounds; pass such options only for input you trust. Inside an `UnmarshalYAML` hook the rule is stricter: options given to `Node.Decode` may only tighten the bounds of the read that called the hook, so `NoLimits()` there does not lift them. The size bounds that concern parsing (`MaxInputBytes`, `MaxDocuments`) do nothing to a document that is already parsed, and options that concern writing are ignored. `KnownFields`, `MergeKeys` and `YAML11Strings` take an optional bool, and the last option wins, so `KnownFields(false)` turns off an earlier `KnownFields()`: ```go src := []byte("name: api\nspec:\n replicas: 2\n replica: 3\n") type Spec struct { Replicas int `yaml:"replicas"` } // Parsed without options, read strictly in one place. doc, err := oneyaml.Parse(src) if err != nil { log.Fatal(err) } var strict Spec err = doc.Get("/spec", &strict, oneyaml.KnownFields()) fmt.Println(errors.Is(err, oneyaml.ErrUnknownField)) // Parsed strictly, read loosely in one place. doc, err = oneyaml.Parse(src, oneyaml.KnownFields()) if err != nil { log.Fatal(err) } var loose Spec if err := doc.Get("/spec", &loose, oneyaml.KnownFields(false)); err != nil { log.Fatal(err) } fmt.Println(loose.Replicas) // Output: // true // 2 ``` A file whose mapping repeats a key still parses, so you can read and edit its other parts. Reading a part that holds the repeated key reports it, as `Unmarshal` does, and a pointer through the repeated key is refused with `ErrAmbiguousKey`, by `Get` and by every edit. `Node.Find` returns an error matching `ErrAmbiguousKey` for such a key and `Node.Lookup` reports `false`, so a lookup agrees with `Get`; `Entries` and `Items` still yield every entry, repeats included. ## Reading while you edit A `Node` is a view of the document as it was when you read it. An edit makes every `Node` read before it stale: `Stale` reports `true`, `Decode` and `Pointer` return an error matching `ErrStaleNode`, and the other methods return zero values. A zero value looks like an answer (`Value` of a stale node is `""`, the same as an empty scalar), so `Node.Err` is the check: it is `nil` for a node you can read, matches `ErrStaleNode` for a stale one, and matches `ErrNotFound` for the zero `Node`, such as the one a failed `Lookup` returns. To look up a key, use `Node.Find(key)`: it returns the value and a `nil` error, or an error that says why there is no value. The error matches `ErrNotFound` when the key is absent, `ErrStaleNode` when the node was read before an edit, and `ErrAmbiguousKey` when the mapping has the key more than once (the same refusal `Get` gives for a pointer through it) or when a merge brings the key in from two places of one source, and it is a `*LimitError` when a merge search was refused ([Merge keys](#merge-keys)). `Node.Lookup` is the short form for when any miss means "no value": it returns `false` in all of these cases, including a key written twice. `Entries` and `Items` still yield every entry, repeats included. Reading the node again after an edit is still the better habit. A `Node` can be compared with `==` and used as a map key. A node read before an edit never equals the node read at the same place after it, because the first one is stale; the [`Node` reference](https://pkg.go.dev/oneyaml.dev/go#Node) gives the rule. ```go doc, err := oneyaml.Parse([]byte("title: ''\nsize: 3\n")) if err != nil { log.Fatal(err) } var title oneyaml.Node if err := doc.Get("/title", &title); err != nil { log.Fatal(err) } fmt.Printf("%q %v\n", title.Value(), title.Err()) if err := doc.Set("/size", 4); err != nil { log.Fatal(err) } // The edit made title stale. Value is "" again, as for an empty scalar, // and only Err says the node can no longer be read. fmt.Printf("%q %v\n", title.Value(), errors.Is(title.Err(), oneyaml.ErrStaleNode)) // Output: // "" // "" true ``` A `range` over `Items` or `Entries` may edit the document, as a loop over a Go map may change the map. After an edit, the loop finds its collection again by the pointer it had before the edit (an edit that inserts or removes items of a list on the way to it moves the pointer along), and goes on at the next position with fresh nodes. The loop ends if the collection is removed or replaced, is no longer the same kind, or has no pointer. Entries added at the end during the loop are yielded, so a body that adds one on every pass never ends. Any number of loops, over one collection or several, can follow edits at once. Changing values as you go is the simple case: ```go doc, err := oneyaml.Parse([]byte("images:\n web: nginx:1.25\n db: postgres:15\n")) if err != nil { log.Fatal(err) } imgs, ok := doc.Root().Lookup("images") if !ok { log.Fatal("no images") } for k, v := range imgs.Entries() { // Each Set makes the nodes read so far stale, but the loop goes on with fresh ones. if err := doc.Set(oneyaml.Pointer("images", k.Value()), v.Value()+"-patched"); err != nil { log.Fatal(err) } } fmt.Print(string(doc.Bytes())) // Output: // images: // web: nginx:1.25-patched // db: postgres:15-patched ``` An edit that adds or removes entries or items moves the later ones, so the loop may skip some or see some twice, as Go allows for a map. For those edits, collect the places first and edit afterwards. `Node.Pointer` gives the RFC 6901 pointer of a node, ready for `Set`, `Get` and `Remove`: ```go doc, err := oneyaml.Parse([]byte("images:\n web: nginx:1.25\n db: postgres:15\n")) if err != nil { log.Fatal(err) } imgs, ok := doc.Root().Lookup("images") if !ok { log.Fatal("no images") } // First collect where each value is, then edit. Replacing values moves // nothing, so every pointer still names the same value. var ptrs, vals []string for _, v := range imgs.Entries() { p, err := v.Pointer() if err != nil { log.Fatal(err) } ptrs = append(ptrs, p) vals = append(vals, v.Value()) } // Then edit by pointer. for i, p := range ptrs { if err := doc.Set(p, vals[i]+"-patched"); err != nil { log.Fatal(err) } } fmt.Print(string(doc.Bytes())) // Nodes read before the edits are stale now. _, err = imgs.Pointer() fmt.Println(imgs.Stale(), errors.Is(err, oneyaml.ErrStaleNode)) // Output: // images: // web: nginx:1.25-patched // db: postgres:15-patched // true true ``` A pointer is an address, not a name for one value. A sequence pointer holds an index, so removing or inserting an item changes what the later indices point to. A rename or a removal higher up changes what every pointer below it addresses. Collected pointers are safe for edits that replace values. To remove several items, remove them from the end of the sequence first, so that each removal leaves the indices before it unchanged: ```go doc, err := oneyaml.Parse([]byte("tags:\n - web\n - old\n - api\n - old\n")) if err != nil { log.Fatal(err) } tags, ok := doc.Root().Lookup("tags") if !ok { log.Fatal("no tags") } var ptrs []string for t := range tags.Items() { if t.Value() != "old" { continue } p, err := t.Pointer() if err != nil { log.Fatal(err) } ptrs = append(ptrs, p) } fmt.Println(ptrs) // Remove from the end: /tags/3, then /tags/1. In the other order, // removing /tags/1 would move "old" from /tags/3 to /tags/2. for i := len(ptrs) - 1; i >= 0; i-- { if err := doc.Remove(ptrs[i]); err != nil { log.Fatal(err) } } fmt.Print(doc) // Output: // [/tags/1 /tags/3] // tags: // - web // - api ``` A patch does not change this. Each operation applies to the result of the ones before it, as RFC 6902 says, so a patch that removes items lists them from the end too. What a patch adds is that the removals happen all or nothing. `Pointer` returns an error matching `ErrNoPointer`, and never a guess, when no pointer leads to the node: a mapping key, anything inside a collection used as a key, or a value whose key occurs twice in its mapping. An alias node has the pointer of the alias itself. A `Node` is not a decode target, because it is a view of a `Document` rather than a value. `Unmarshal` into a `Node` returns a `*TypeError`; read a node with `Document.Get(pointer, &node)`. `Marshal` writes a `Node` as the value it holds: ```go src := []byte("name: api\nspec: {replicas: 2, tier: web}\n") var n oneyaml.Node err := oneyaml.Unmarshal(src, &n) var te *oneyaml.TypeError fmt.Println(errors.As(err, &te)) doc, err := oneyaml.Parse(src) if err != nil { log.Fatal(err) } if err := doc.Get("/spec", &n); err != nil { log.Fatal(err) } fmt.Println(n.Kind(), n.Len()) out, err := oneyaml.Marshal(struct { Spec oneyaml.Node `yaml:"spec"` }{n}) if err != nil { log.Fatal(err) } fmt.Print(string(out)) // Output: // true // mapping 2 // spec: // replicas: 2 // tier: web ``` ## Streams and encodings A file with several documents is one `Document`. `Doc(i)` is a view of document *i* that shares the stream, and edits through it change the stream. ```go doc, err := oneyaml.Parse([]byte("a: 1\n---\nb: 2\n")) if err != nil { log.Fatal(err) } if err := doc.Doc(1).Set("/c", 3); err != nil { log.Fatal(err) } fmt.Println(doc.Len()) fmt.Print(string(doc.Bytes())) // Output: // 2 // a: 1 // --- // b: 2 // c: 3 ``` UTF-16 and UTF-32 input is read, and the document is written back in its own encoding and byte order mark. ## Coming from yaml.v3 yaml.v3 changes a document by walking a `yaml.Node` tree and encoding the whole tree again, so the encoder decides the indentation and layout rather than your file. The equivalent here is one call: ```go src := "# service settings\nname: api # public name\nreplicas: 2\n" doc, err := oneyaml.Parse([]byte(src)) if err != nil { log.Fatal(err) } if err := doc.Set("/replicas", 3); err != nil { log.Fatal(err) } fmt.Print(string(doc.Bytes())) // Output: // # service settings // name: api # public name // replicas: 3 ``` Renaming a key and changing a comment work the same way: where yaml.v3 code changes a key node's `Value` or a node's `HeadComment` and encodes the tree again, call [`Rename`](#renaming-a-key) or [`SetHeadComment` and `SetLineComment`](#comments). More in [Migrating from yaml.v3](https://recursiveai.org/oneyaml/go/docs/migrate-yaml-v3/). # Patches How to apply a patch to a YAML file. The operations are those of JSON Patch ([RFC 6902](https://www.rfc-editor.org/rfc/rfc6902)), the paths are JSON Pointers ([RFC 6901](https://www.rfc-editor.org/rfc/rfc6901)), and merge patches follow JSON Merge Patch ([RFC 7396](https://www.rfc-editor.org/rfc/rfc7396)), but they are applied to YAML with YAML's data model, not to JSON. The edits follow the rules in [Editing](https://recursiveai.org/oneyaml/go/docs/editing/): only the values a patch touches change, in their own style, and comments elsewhere are kept. There are three entry points on `Document`: | Method | Takes | |---|---| | `ApplyPatch(rfc6902 []byte)` | an RFC 6902 patch as JSON text, or as a YAML sequence of operations ([details](#patches-written-in-yaml)) | | `Apply(ops ...PatchOp)` | the same operations as Go values: `PatchAdd`, `PatchRemove`, `PatchReplace`, `PatchMove`, `PatchCopy`, `PatchTest` | | `ApplyMergePatch(rfc7396 []byte)` | an RFC 7396 merge patch as JSON text, or as YAML ([details](#patches-written-in-yaml)) | ## YAML rules at a glance Four rules follow from applying the RFCs to YAML: - **`test` compares YAML values.** The integer `1` and the float `1.0` are different values, so a `test` of one against the other fails, where RFC 6902 would call them equal as JSON numbers ([the test operation](#the-test-operation)). - **A pointer token matches a key by its scalar text.** `/1` finds the integer key `1` as well as the string key `"1"` ([Pointers](#pointers)). - **A write through an alias is refused** with `ErrAliasWrite`, because it would change every place that shares the anchored node. Patch the anchored node instead ([Aliases](#aliases)). - **Text that starts like JSON is JSON.** When the first significant byte of the patch is `[` or `{`, the patch is read as JSON and only JSON, so a flow-style YAML patch such as `[{op: test, path: /a, value: 1}]` is refused because its keys are not quoted. Write it as JSON, or as a YAML block list ([Patches written in YAML](#patches-written-in-yaml)). ```go doc, err := oneyaml.Parse([]byte("replicas: 1\n")) if err != nil { log.Fatal(err) } // 1 is an integer and 1.0 a float, so this test fails. err = doc.ApplyPatch([]byte(`[{"op": "test", "path": "/replicas", "value": 1.0}]`)) fmt.Println(errors.Is(err, oneyaml.ErrTestFailed)) // Text that starts with [ is read as JSON, and these keys are not quoted. err = doc.ApplyPatch([]byte(`[{op: test, path: /replicas, value: 1}]`)) fmt.Println(errors.Is(err, oneyaml.ErrInvalidPatch)) // The same operation as a YAML block list. err = doc.ApplyPatch([]byte("- {op: test, path: /replicas, value: 1}\n")) fmt.Println(err) // Output: // true // true // ``` ## A patch is atomic If any operation fails, the document is unchanged, including the operations before the one that failed. The error is an `*EditError` whose `Index` is the position of the failing operation and whose kind you match with `errors.Is`. ```go doc, err := oneyaml.Parse([]byte(`# service name: api replicas: 2 # raised by the release job tags: [web] `)) if err != nil { log.Fatal(err) } err = doc.ApplyPatch([]byte(`[ {"op": "test", "path": "/name", "value": "api"}, {"op": "replace", "path": "/replicas", "value": 3}, {"op": "add", "path": "/tags/-", "value": "edge"}, {"op": "copy", "from": "/name", "path": "/title"} ]`)) if err != nil { log.Fatal(err) } fmt.Print(string(doc.Bytes())) // A failing test rejects the whole patch. err = doc.ApplyPatch([]byte(`[ {"op": "replace", "path": "/replicas", "value": 9}, {"op": "test", "path": "/name", "value": "web"} ]`)) var ee *oneyaml.EditError fmt.Println(errors.Is(err, oneyaml.ErrTestFailed), errors.As(err, &ee) && ee.Index == 1) fmt.Print(string(doc.Bytes())) // Output: // # service // name: api // replicas: 3 # raised by the release job // tags: [web, edge] // title: api // true true // # service // name: api // replicas: 3 # raised by the release job // tags: [web, edge] // title: api ``` The typed form takes the same operations: ```go doc, err := oneyaml.Parse([]byte("a: 1\nb: 2\n")) if err != nil { log.Fatal(err) } err = doc.Apply( oneyaml.PatchTest("/a", 1), oneyaml.PatchMove("/b", "/c"), oneyaml.PatchAdd("/d", []string{"x", "y"}), ) if err != nil { log.Fatal(err) } fmt.Print(string(doc.Bytes())) // Output: // a: 1 // c: 2 // d: // - x // - y ``` `add` inserts before an index, as RFC 6902 says; `Document.Set` assigns. `move` removes then adds, except that between two mappings the destination is written first, so a mapping never collapses to `{}` halfway. ## Pointers Operations address values with [RFC 6901](https://www.rfc-editor.org/rfc/rfc6901) pointers. A token matches a mapping key by the key's text, so `/1` finds the integer key `1` as well as the string key `"1"`: ```go doc, err := oneyaml.Parse([]byte("1: one\n\"2\": two\n")) if err != nil { log.Fatal(err) } err = doc.ApplyPatch([]byte(`[ {"op": "replace", "path": "/1", "value": "uno"}, {"op": "replace", "path": "/2", "value": "dos"} ]`)) if err != nil { log.Fatal(err) } fmt.Print(string(doc.Bytes())) // Output: // 1: uno // "2": dos ``` Inside a token, `/` is written `~1` and `~` is written `~0`. When a key comes from data, build the path with `oneyaml.Pointer`, which escapes each token: `oneyaml.PatchRemove(oneyaml.Pointer("metadata", "labels", key))` removes the label `app.kubernetes.io/name` when `key` holds it ([Editing](https://recursiveai.org/oneyaml/go/docs/editing/) shows the pointer it builds). A pointer addresses the keys written in the file. With `MergeKeys()`, `test` and the `from` of `copy` also read a key that a `<<` merge brings in, and `remove`, `replace` and `move` of such a key are refused with `ErrAliasWrite`; [Merge keys](https://recursiveai.org/oneyaml/go/docs/editing/#merge-keys) has the rules. If two keys have the same text the pointer is ambiguous and the operation fails with `ErrAmbiguousKey`. A key that is itself a collection cannot be addressed by a pointer. A pointer that is not valid fails with `ErrInvalidPointer`; a patch that is not valid fails with `ErrInvalidPatch`. Patch text must be UTF-8. ## Patches written in YAML `ApplyPatch` also takes a patch kept as a YAML file: a block list of operations with the same members as the JSON form. It applies exactly as the JSON patch with the same operations does: ```go doc, err := oneyaml.Parse([]byte("replicas: 2\nimage: nginx:1.25\n")) if err != nil { log.Fatal(err) } patch := ` - op: replace path: /replicas value: 5 - op: add path: /labels value: tier: web ` if err := doc.ApplyPatch([]byte(patch)); err != nil { log.Fatal(err) } fmt.Print(doc) // Output: // replicas: 5 // image: nginx:1.25 // labels: // tier: web ``` The first significant byte, after white space and a byte order mark, decides how the text is read. Text that starts with `[` or `{` is JSON, and only JSON, so its errors are JSON errors. A flow-style YAML patch such as `[{op: add, path: /a, value: 1}]` is therefore refused, because its keys are not quoted; write it as JSON. All other text is read as YAML, so a block list whose items are flow mappings (`- {op: add, path: /a, value: 1}`) is read. The non-specific tag `!` is allowed and makes a scalar a string, so `value: ! 12` writes the string `"12"`. A YAML patch fails with `ErrInvalidPatch` when it: - holds more than one document, or is not a list; - uses an alias, a tag outside the core schema such as `!foo`, or a collection as a key, none of which JSON can express; - names a key twice in a mapping, judged by value as the decoder judges it, so `{1: a, 0x1: b}` and `{null: 1, ~: 2}` are refused; - has a scalar that cannot be read, such as a double-quoted escape that is not a Unicode character; - is not valid YAML. The error gives the line and column. A YAML patch nested deeper than the depth limit fails with a `*LimitError`. `ApplyMergePatch` reads YAML the same way: text that starts with `{` or `[`, or is valid JSON, is JSON, and any other text is one YAML document of any kind (a mapping, usually) with the same refusals as a YAML list of operations. The members of a merge patch are named by strings, so a mapping key that is not a string, such as `1: 5`, `true: 5` or an empty key, is refused with `ErrInvalidPatch` at any depth, with its line and column. Quote it (`"1": 5`) to name the member `1`. ```go doc, err := oneyaml.Parse([]byte(`name: api limits: cpu: 1 # cores mem: 2 `)) if err != nil { log.Fatal(err) } err = doc.ApplyMergePatch([]byte(`limits: cpu: 4 mem: null tier: gold `)) if err != nil { log.Fatal(err) } fmt.Print(string(doc.Bytes())) // Output: // name: api // limits: // cpu: 4 # cores // tier: gold ``` ## Aliases Reads follow aliases. Writes do not: a patch that would write through an alias is refused with `ErrAliasWrite`, because it would change every place that shares the anchored node. Patch the anchored node instead, and the change shows through every alias. ```go doc, err := oneyaml.Parse([]byte("base: &b\n x: 1\ncopy: *b\n")) if err != nil { log.Fatal(err) } err = doc.ApplyPatch([]byte(`[ {"op": "replace", "path": "/base/x", "value": 2}, {"op": "replace", "path": "/copy/x", "value": 3} ]`)) fmt.Println(errors.Is(err, oneyaml.ErrAliasWrite)) fmt.Print(string(doc.Bytes())) // Output: // true // base: &b // x: 1 // copy: *b ``` Removing or moving an anchored node that an alias still uses is refused with `ErrAnchorInUse`. When a `copy` or `add` takes a value that holds aliases, the value is written at its destination without its anchor, and an alias that would no longer have its anchor in front of it is written out as a copy. Written-out aliases count against the alias budget described in [Limits](https://recursiveai.org/oneyaml/go/docs/limits/). ## One document per patch A patch applies to the document the `Document` addresses, which is the first document of a stream unless you select another with `Doc(i)`. ```go doc, err := oneyaml.Parse([]byte("a: 1\n---\na: 1\n")) if err != nil { log.Fatal(err) } if err := doc.Doc(1).ApplyPatch([]byte(`[{"op": "replace", "path": "/a", "value": 2}]`)); err != nil { log.Fatal(err) } fmt.Print(string(doc.Bytes())) // Output: // a: 1 // --- // a: 2 ``` ## The test operation `test` compares by the YAML data model under the Core schema, not by JSON text. `1` and `1.0` are different, `0x1F` equals `31`, and a mapping key matches a JSON object key only if it is a string. This is stricter than RFC 6902 section 4.6, which compares JSON values. ```go doc, err := oneyaml.Parse([]byte("n: 1\n")) if err != nil { log.Fatal(err) } fmt.Println(doc.Apply(oneyaml.PatchTest("/n", 1))) err = doc.Apply(oneyaml.PatchTest("/n", 1.0)) fmt.Println(errors.Is(err, oneyaml.ErrTestFailed)) // Output: // // true ``` ## Merge patches A merge patch deletes a member with `null`, merges an object into the mapping it meets and replaces anything else, so an array replaces the whole sequence. An empty object changes nothing. Deletions are applied after the other members of the same mapping. With `MergeKeys()`, a member `null` for a key that only a `<<` merge source has is refused with `ErrAliasWrite` ([Merge keys](https://recursiveai.org/oneyaml/go/docs/editing/#merge-keys)). Merging an object into an alias is refused: ```go doc, err := oneyaml.Parse([]byte("base: &b {x: 1}\ncopy: *b\n")) if err != nil { log.Fatal(err) } err = doc.ApplyMergePatch([]byte(`{"copy": {"x": 2}}`)) fmt.Println(errors.Is(err, oneyaml.ErrAliasWrite)) // Output: // true ``` A complete merge patch walkthrough, with the comments kept, is in the reference for [`Document.ApplyMergePatch`](https://pkg.go.dev/oneyaml.dev/go#Document.ApplyMergePatch). ## Bounds A patch can ask for a lot of work: `copy` can double a document at every step, and some edits re-read a large part of the file. Two limits bound a patch, and both apply under the default limits: - the bytes that copies and written-out aliases may produce are charged to the alias-bytes limit; - the text that a patch's edits have to read again is bounded by `LimitSet.MaxPatchWorkRatio`. With a `MaxInputBytes` limit (as `Untrusted()` sets), the patch text itself may be at most an eighth of it (at least 64 KiB, and never more than the limit), because a patch is read into a list of operations that takes far more memory than the same amount of document, and the result may not exceed the input-size limit. The default limits set no input bound. A patch that goes past a bound fails with a `*LimitError` and changes nothing. See [Limits](https://recursiveai.org/oneyaml/go/docs/limits/). # Limits Limits stop input from using more time or memory than you intended. By default, nesting depth, alias expansion, the digits of a number and the work of a patch are bounded. Input size, documents per stream and scalar length are bounded only when you select `Untrusted()`; select it, and size it for your data, for any input you do not control. This page lists each limit, the presets, how options combine, and what the limits do not cover: your own hooks and the compat packages among them. Every call that reads input is limited: `Unmarshal`, `Decoder`, `Parse` and the `Document` methods that follow aliases or apply patches. Aliases are counted wherever they are followed, so reading from a `Document` is bounded in the same way as decoding. `Get`, `Decode` and `Node.Decode` use the limits the `Document` was parsed with, and a limit option given to one of them changes the limits for that read only ([Reading values back](https://recursiveai.org/oneyaml/go/docs/editing/#reading-values-back)). The options are: | Option | Effect | |---|---| | none | `DefaultLimits()`: generous bounds that real documents do not reach but that stop alias bombs and runaway nesting | | `Untrusted()` | `UntrustedLimits()`: tighter bounds, and caps on input, scalar and stream size. Use it for input from a source you do not control | | `Limits(LimitSet)` | changes only the fields you set, on top of the options before it (the defaults when there are none). A field left at zero keeps the bound it had, and `NoBound` (or any negative value) lifts that one bound. `Limits(LimitSet{})` changes nothing | | `NoLimits()` | no bound at all, including depth when writing. Use it only for input you trust | Options apply in order. `Untrusted()` and `NoLimits()` replace every bound; a `Limits` after them changes only the fields it sets. So `Untrusted(), Limits(LimitSet{MaxDepth: 50})` keeps every untrusted bound and tightens depth, and `NoLimits(), Limits(LimitSet{MaxInputBytes: 1 << 20})` bounds the input size and nothing else. The presets side by side, as the code defines them. A field that shows 0 has no bound in that preset: ```go fmt.Printf("%+v\n", oneyaml.DefaultLimits()) fmt.Printf("%+v\n", oneyaml.UntrustedLimits()) // Output: // {MaxDepth:10000 MaxAliasNodes:1000000 MaxAliasBytes:67108864 MaxInputBytes:0 MaxScalarBytes:0 MaxDocuments:0 MaxNumberDigits:100000 MaxPatchWorkRatio:256} // {MaxDepth:1000 MaxAliasNodes:100000 MaxAliasBytes:8388608 MaxInputBytes:67108864 MaxScalarBytes:16777216 MaxDocuments:10000 MaxNumberDigits:4300 MaxPatchWorkRatio:32} ``` ## Which options apply where One `Option` type serves every entry point, and an option that does not concern a call is ignored, never an error. The patch methods (`ApplyPatch`, `Apply`, `ApplyMergePatch`) take no options: they use the options the `Document` was parsed with. `Untrusted()` and `NoLimits()` set every `LimitSet` field, so they reach each entry point as those fields do. | Option | `Unmarshal`, `Decoder` | `Parse` | `Get`, `Decode`, `Node.Decode` | Patches (options of `Parse`) | `Marshal`, `Encoder` | |---|---|---|---|---|---| | `KnownFields` | applies | kept for later reads | applies to this read | no effect | ignored | | `MergeKeys` | applies | kept for reads, pointers and edits | applies to this read | `test` and the `from` of `copy` read merged keys | ignored | | `YAML11Strings` | ignored | kept for edits | ignored | quotes new strings | ignored (`Marshal` always quotes them) | | `Indent` | ignored | ignored (edits follow the file) | ignored | ignored | applies | | `MaxDepth` | applies | applies, and kept | applies to this read | patch text and values | applies | | `MaxAliasNodes`, `MaxAliasBytes` | applies | kept: parsing expands no alias | applies to this read | copies and written-out aliases | ignored | | `MaxInputBytes` | applies; a `Decoder` counts the whole stream | applies, and kept for edits | no effect: the text is already parsed | patch text and the patched document | ignored | | `MaxScalarBytes` | applies | applies, and kept | applies to this read | not checked on the values a patch writes | ignored | | `MaxDocuments` | `Decoder` only: `Unmarshal` reads one document | applies | no effect: the text is already parsed | no effect | ignored | | `MaxNumberDigits` | applies | kept: parsing converts no number | applies to this read | not checked on patch values; a later read of them is | numbers from a `MarshalJSON` method | | `MaxPatchWorkRatio` | ignored | kept for patches | ignored | applies | ignored | "Kept" means the `Document` remembers the option and uses it where the next columns say. A bound given to a read replaces the document's own for that read, looser as well as tighter; inside an `UnmarshalYAML` hook it can only tighten ([Reading values back](https://recursiveai.org/oneyaml/go/docs/editing/#reading-values-back)). `Set`, `Rename` and the comment setters follow the options of `Parse` as patches do, for `MaxDepth`, `MaxInputBytes`, `MergeKeys` and `YAML11Strings`. ## Each limit | `LimitSet` field | What it bounds | If it is exceeded | |---|---|---| | `MaxDepth` | nesting of collections, in input and when writing | `*LimitError`, `Limit` is `LimitMaxDepth` | | `MaxAliasNodes` | nodes produced by expanding aliases; with `MergeKeys()`, also the keys one pointer lookup, `Node.Find` or `Node.Lookup` reads in `<<` merge sources ([Merge keys](https://recursiveai.org/oneyaml/go/docs/editing/#merge-keys)) | `*LimitError`, also from `Node.Find`; `Node.Lookup` reports it as `false`, like an absent key, so use `Node.Find` to tell the two apart | | `MaxAliasBytes` | scalar bytes produced by expanding aliases; also the bytes a patch copies | `*LimitError` | | `MaxInputBytes` | the size of the input, and the size of a document after a patch, `Set`, `Rename` or a comment setter, counted in the encoding the document is written in (a UTF-16 document with its byte order mark, say); the text of one patch or merge patch may be an eighth of it, but never less than 64 KiB and never more than the limit ([Bounds](https://recursiveai.org/oneyaml/go/docs/patches/#bounds)) | `*LimitError` | | `MaxScalarBytes` | the length of one scalar | `*LimitError` | | `MaxDocuments` | documents in one stream | `*LimitError` | | `MaxNumberDigits` | digits of an integer or float scalar when it is converted to a number, including a plain number used as a key in a part of the input the target ignores, because such keys are compared as numbers to find repeated keys; text kept as text is never limited | `*LimitError` | | `MaxPatchWorkRatio` | text that one patch's edits may read again, as a multiple of the document plus the patch (and 16 MiB on top) | `*LimitError`, the document is unchanged | An alias bound left at its default rises with the document: it is at least the figure shown above and at least ten times the document's own nodes or bytes (two times with `Untrusted`), so a large document that reuses its anchors is not refused. An alias bound you set with `Limits` applies exactly as given. A bound that is hit is a `*LimitError`; an edit or patch returns it inside an `*EditError`, where `errors.As` and `errors.Is` still find it. `Limit` names the bound by its `LimitSet` field (`LimitMaxDepth`, `LimitMaxAliasNodes` and so on), `Max` holds it, and `What` describes it in words for messages. `Option` names the option and field that raise it, in the form `Limits (MaxDepth)`. It is empty for the three bounds that no option changes: the nesting of a patch value (10,000 levels), the nesting of typed Go values (100,000 levels) and the 2 GiB size of a `Document`. The compat packages take no limit options, so their limit errors have an empty `Option` too. `Path` holds the pointer to the value where the bound was hit, such as the alias, scalar or number being read. It is empty where there is no single value: the limits `Parse` reports, the input size, the document count, the nesting that the parser stops at, and the size of the text an edit leaves. `errors.Is` matches `ErrLimit` for any limit and a sentinel for each one, such as `ErrMaxDepth`, so a program need not read the fields to tell limits apart. ```go bomb := "a: &a [x, x, x, x]\nb: &b [*a, *a, *a, *a]\nc: [*b, *b, *b, *b]\n" var v any err := oneyaml.Unmarshal([]byte(bomb), &v, oneyaml.Limits(oneyaml.LimitSet{MaxAliasNodes: 20})) var le *oneyaml.LimitError if errors.As(err, &le) { fmt.Println(le.Limit, le.Max) } // Output: MaxAliasNodes 20 ``` To tell limits apart, match the sentinel or switch on `Limit`: ```go deep := strings.Repeat("[", 100) + strings.Repeat("]", 100) err := oneyaml.Unmarshal([]byte(deep), new(any), oneyaml.Limits(oneyaml.LimitSet{MaxDepth: 50})) fmt.Println(errors.Is(err, oneyaml.ErrLimit), errors.Is(err, oneyaml.ErrMaxDepth), errors.Is(err, oneyaml.ErrMaxInputBytes)) var le *oneyaml.LimitError if errors.As(err, &le) { switch le.Limit { case oneyaml.LimitMaxDepth: fmt.Println("too deep:", le.Max, le.Option) } } // Output: // true true false // too deep: 50 Limits (MaxDepth) ``` ## Changing one bound A `LimitSet` passed to `Limits` changes only the fields you set. Here `MaxInputBytes` adds a bound on input size, and the default depth bound stays in force: ```go opt := oneyaml.Limits(oneyaml.LimitSet{MaxInputBytes: 1 << 20}) large := []byte("a: " + strings.Repeat("x", 2<<20) + "\n") deep := []byte(strings.Repeat("[", 10001) + strings.Repeat("]", 10001)) var v any var le *oneyaml.LimitError err := oneyaml.Unmarshal(large, &v, opt) if errors.As(err, &le) { fmt.Println(le.Limit, le.Max) } err = oneyaml.Unmarshal(deep, &v, opt) if errors.As(err, &le) { fmt.Println(le.Limit, le.Max) } // Output: // MaxInputBytes 1048576 // MaxDepth 10000 ``` `NoBound` lifts one bound and keeps the rest. Any negative value does the same; `NoBound` names it: ```go deep := []byte(strings.Repeat("[", 10001) + strings.Repeat("]", 10001)) var v any err := oneyaml.Unmarshal(deep, &v, oneyaml.Limits(oneyaml.LimitSet{MaxDepth: oneyaml.NoBound})) fmt.Println(err) // Output: ``` `Limits` after `Untrusted()` keeps the untrusted bounds and changes only what it names. Here depth is tightened to 50, and the untrusted bound of 4300 digits on a number still applies: ```go opts := []oneyaml.Option{oneyaml.Untrusted(), oneyaml.Limits(oneyaml.LimitSet{MaxDepth: 50})} deep := strings.Repeat("[", 100) + strings.Repeat("]", 100) long := strings.Repeat("9", 5000) var v any var le *oneyaml.LimitError for _, in := range []string{deep, long} { err := oneyaml.Unmarshal([]byte(in), &v, opts...) if errors.As(err, &le) { fmt.Println(le.Limit, le.Max) } } // Output: // MaxDepth 50 // MaxNumberDigits 4300 ``` ## Untrusted input `Untrusted()` lowers the depth and alias bounds and adds a cap on input size, scalar size, documents per stream, the digits of a number, and the work and text size of a patch. In the example, 2000 nested sequences pass under the default limits and fail under `Untrusted()`: ```go deep := strings.Repeat("[", 2000) + strings.Repeat("]", 2000) var v any // The default depth limit lets this through; Untrusted does not. fmt.Println(oneyaml.Unmarshal([]byte(deep), &v)) err := oneyaml.Unmarshal([]byte(deep), &v, oneyaml.Untrusted()) var le *oneyaml.LimitError if errors.As(err, &le) { fmt.Println(le.Limit, le.Max, le.Option) } // Output: // // MaxDepth 1000 Limits (MaxDepth) ``` ## Loading a configuration file strictly To load a configuration file and stop on anything unexpected, combine three things: - `KnownFields()`, so that a misspelt key is an error instead of being skipped; - `Untrusted()` with a `MaxInputBytes` sized for your files; - the positions in the errors, which `TypeError.All` lists one per problem. ```go type Config struct { Name string `yaml:"name"` Replicas int `yaml:"replicas"` } src := []byte("name: api\nreplica: 3\nreplicas: two\n") // Decode into a fresh value and use it only when there is no error: a read // that reports mismatches has still filled the fields it could. var cfg Config err := oneyaml.Unmarshal(src, &cfg, oneyaml.KnownFields(), // a misspelt key is an error, not skipped oneyaml.Untrusted(), // bounds on size, depth, aliases and numbers oneyaml.Limits(oneyaml.LimitSet{MaxInputBytes: 1 << 20}), // sized for a config file ) var te *oneyaml.TypeError switch { case errors.As(err, &te): for _, e := range te.All() { fmt.Printf("line %d, column %d: %s\n", e.Line, e.Column, e.Msg) } fmt.Println("unknown field:", errors.Is(err, oneyaml.ErrUnknownField)) case err != nil: // A *SyntaxError has a position too, and a *LimitError names the bound. log.Fatal(err) default: fmt.Println(cfg.Name, cfg.Replicas) } // Output: // line 2, column 1: field "replica" not found in type oneyaml_test.Config // line 3, column 11: cannot unmarshal !!str `two` into int // unknown field: true ``` The compat packages take no options at all, so neither `KnownFields()` nor `Untrusted()` reaches them; `compat/k8syaml` has `UnmarshalStrict` and `compat/yamlv3` has `Decoder.KnownFields` for unknown keys. Bound the input yourself before you hand it over, with a length check or with `io.LimitReader`. Read one byte more than the bound and refuse input that reaches it, because a reader cut off at the bound can leave a shorter document that is still valid YAML: ```go const maxBytes = 1 << 20 r := strings.NewReader("replicas: 3\n") // a file or a request body data, err := io.ReadAll(io.LimitReader(r, maxBytes+1)) if err != nil { log.Fatal(err) } if len(data) > maxBytes { log.Fatal("input is larger than 1 MiB") } var cfg struct { Replicas int `json:"replicas"` } if err := yaml.Unmarshal(data, &cfg); err != nil { log.Fatal(err) } fmt.Println(cfg.Replicas) // Output: 3 ``` The example imports `yaml "oneyaml.dev/go/compat/k8syaml"`, `io` and `strings`. A length check bounds only the size: depth and alias expansion stay at the default limits, which the compat packages cannot tighten. `Unmarshal` reads the first document and ignores the rest, and an empty input leaves the value unchanged with a nil error. To require exactly one document, read with `NewDecoder` and check that a second `Decode` returns `io.EOF`. To refuse an empty file, check the value or the input before you use it. ## Trusted input `NoLimits()` removes every bound. Nesting that the default depth limit refuses is read once it is off: ```go deep := strings.Repeat("[", 10001) + strings.Repeat("]", 10001) var v any err := oneyaml.Unmarshal([]byte(deep), &v) var le *oneyaml.LimitError fmt.Println(errors.As(err, &le)) fmt.Println(oneyaml.Unmarshal([]byte(deep), &v, oneyaml.NoLimits())) // Output: // true // ``` ## What limits do not cover - **Memory held by a `Document`.** A `Document` keeps the text and an index of it. For ordinary files that is a few times the text; for dense input, such as long lists of tiny scalars, it is an order of magnitude more. Without `MaxInputBytes` nothing caps it, so set it (or use `Untrusted()`) for outside input. - **Documents of 2 GiB or more.** A `Document` addresses its text with 32-bit offsets, so `Parse` refuses input of 2 GiB or more, and an edit or patch that would grow the text past that is refused. Both report `LimitMaxInputBytes` with an empty `Option`, because no option lifts this bound. Keep documents far below that. - **`Marshal` output.** `Marshal` writes a value that is shared in several places once per place, as `encoding/json` does, so the output can be far larger than the value in memory. A value that contains itself is an error. When writing, `MaxDepth` bounds nesting and `MaxNumberDigits` bounds the numbers a `MarshalJSON` method returns; no other limit applies. - **The compat packages.** `compat/yamlv3` and `compat/k8syaml` always run with the default limits and have no `Untrusted()` mode or other option to change them. For input from outside, read with the `oneyaml` package and `Untrusted()`. - **Your own hooks.** An `Unmarshaler` that calls `Node.Decode`, or a func-style `UnmarshalYAML` that calls the function it is given, shares the alias budget of the call that invoked it and keeps its options, such as `KnownFields()` or a bound given to a `Document` read. Options a hook passes to `Node.Decode` can only tighten those bounds: for each one the stricter of the caller's and the hook's applies, so `NoLimits()` inside a hook does not lift them. The code you write in a hook, including `UnmarshalText` and `UnmarshalJSON`, is not limited. The safety posture and how to report a problem are in [SECURITY.md](https://github.com/recursive-ai-oss/oneyaml-go/blob/7a4c8e39f041b945ee5f121c055f03bc2d7d01d9/SECURITY.md). # Migrating from yaml.v3 Two ways to move from `gopkg.in/yaml.v3` (or `go.yaml.in/yaml/v3`): keep your code and swap the import to the `compat/yamlv3` adapter, or move to the `oneyaml` API. The first keeps your call sites, and the [checklist](#check-after-the-swap) below covers where its behaviour can differ. The second reads YAML 1.2 with the Core schema and gives you minimal-diff edits. ## Migration audit Search your code and your data for these before you move. Each item links to its details. - **Hooks and fields that use gopkg.in/yaml.v3's own `Node`.** An `UnmarshalYAML(*yaml.Node) error` that takes the original package's node, often in a module you do not own, and a field of the original `yaml.Node` type are refused with an error naming the type, by `compat/yamlv3` and by the `oneyaml` package alike. Read such a type through a wrapper type of your own whose hook the adapter can call, or keep yaml.v3 for it ([hooks you do not own](#check-after-the-swap)). - **YAML 1.1 numbers and booleans.** The `oneyaml` package reads `yes`, `no`, `on` and `off` as strings and `0755` as the integer 755; yaml.v3 and `compat/yamlv3` read the 1.1 forms ([behaviour that differs](#behaviour-that-differs)). - **Merge keys.** `<<` is an ordinary key in the `oneyaml` package unless you pass `MergeKeys()`; yaml.v3 and `compat/yamlv3` always merge ([behaviour that differs](#behaviour-that-differs)). - **`json` tags.** The `oneyaml` package names a field without a `yaml` tag by its `json` tag, where yaml.v3 uses the lower-cased Go name, so a field that has only a `json` tag reads a different key ([the API table](#move-to-the-oneyaml-api)). - **Hook precedence.** The `oneyaml` package tries a type's methods in a fixed order and ends with `UnmarshalJSON` and `MarshalJSON`, which yaml.v3 ignores, so a type that yaml.v3 filled field by field may now read or write itself through its JSON methods ([Hooks](#hooks)). `compat/yamlv3` ignores them, as yaml.v3 does. - **Programs that read the file with YAML 1.1 rules.** If yaml.v3, yaml.v2 or sigs.k8s.io/yaml code also reads the files you edit, parse with `YAML11Strings()` so that a new `yes` or `0755` is quoted ([Editing](https://recursiveai.org/oneyaml/go/docs/editing/#when-the-style-cannot-hold-the-value)). ## Swap the import ```sh # before: import yaml "gopkg.in/yaml.v3" # after: import yaml "oneyaml.dev/go/compat/yamlv3" go get oneyaml.dev/go@latest ``` The package is named `yaml` and has yaml.v3's API: `Unmarshal`, `Marshal`, `NewDecoder` and `NewEncoder` with their methods, `Node` with its methods and the `Kind` and `Style` constants, `Marshaler`, `Unmarshaler` and the older function-style unmarshaler, `IsZeroer` and `TypeError`, and the struct tag options `omitempty`, `flow` and `inline`. Call sites that use them compile unchanged. It is a compatibility adapter, not a copy of yaml.v3. It reproduces what yaml.v3 documents: plain scalars resolve the yaml.v3 way, so `0755` is an octal number and `yes` is accepted by a bool field, merge keys are on, and a type error does not stop decoding. The swap changes your imports only. A type from another module that reads itself with yaml.v3's `UnmarshalYAML(*yaml.Node) error` still takes the original `*yaml.Node`, and neither the adapter nor the `oneyaml` package can call that method; both report an error naming the type instead of filling it. See [hooks you do not own](#check-after-the-swap) before you rely on the swap. ```go type Service struct { Name string `yaml:"name"` Mode int `yaml:"mode"` Debug bool `yaml:"debug"` } var s Service if err := yaml.Unmarshal([]byte("name: api\nmode: 0755\ndebug: yes\n"), &s); err != nil { fmt.Println(err) return } fmt.Printf("%+v\n", s) out, err := yaml.Marshal(s) if err != nil { log.Fatal(err) } fmt.Print(string(out)) // Output: // {Name:api Mode:493 Debug:true} // name: api // mode: 493 // debug: true ``` Type errors are collected, as in yaml.v3: ```go var v struct { Port int Name string Count int } err := yaml.Unmarshal([]byte("port: eighty\nname: api\ncount: x\n"), &v) var te *yaml.TypeError if e, ok := err.(*yaml.TypeError); ok { te = e } for _, msg := range te.Errors { fmt.Println(msg) } fmt.Println(v.Name) // Output: // line 1: cannot unmarshal !!str `eighty` into int // line 3: cannot unmarshal !!str `x` into int // api ``` The package differs from yaml.v3 where yaml.v3 is wrong: it reads valid YAML that yaml.v3's scanner refuses, returns an error where yaml.v3 panics, never turns a comment into content, and keeps the default limits of the `oneyaml` package. The [package documentation](https://pkg.go.dev/oneyaml.dev/go/compat/yamlv3) lists each difference and each gap. Reading with `yaml.Node` and writing it back puts comments on the same nodes as yaml.v3 in the common layouts; to edit a file, see below. ### Check after the swap Run your tests, and look at these places in particular: - **Hooks.** `UnmarshalYAML(*yaml.Node) error`, the older `UnmarshalYAML(func(interface{}) error) error` and `MarshalYAML() (interface{}, error)` work as in yaml.v3, with the adapter's own `yaml.Node`. A method that takes the original package's `*yaml.Node`, for example in a package of yours that still imports `gopkg.in/yaml.v3`, does not match. yaml.v3 itself skips a method of the wrong type; the adapter returns an error naming the type and the method instead, so validation is never dropped silently. Change that import too. The same holds when writing: a `MarshalYAML` method with any other signature makes `Marshal` return an error naming the type and the method, where yaml.v3 writes the value field by field. - **Hooks you do not own.** A type from a module you do not control, whose `UnmarshalYAML` takes `gopkg.in/yaml.v3`'s `*yaml.Node`, keeps that signature after you swap your own import. The adapter can't call the method, and reading into such a type returns an error that names the type and the method (the `oneyaml` package reports it too, as a `*TypeError`); it does not fill the type field by field, which would skip whatever the method checked or converted. (yaml.v3 would skip a method of an unknown shape without a word; the adapter refuses on purpose.) Swapping the import does not fix this. Read such a type through a wrapper type of your own: give the wrapper a hook the adapter or the `oneyaml` package calls, and build the dependency's value inside it. Or keep reading that type with yaml.v3 until its module moves. List the third-party types your configuration reads and check each one. Writing is less affected: `MarshalYAML() (interface{}, error)` has the same signature in every package, so the adapter calls those hooks. The exception is a hook that returns gopkg.in/yaml.v3's own `*yaml.Node`, or a field of that type. The adapter cannot read that node, so `Marshal` returns an error that names the type instead of writing the node's fields (`kind:`, `value:` and so on). Build the adapter's `yaml.Node` in the hook instead. Reading into yaml.v3's `Node` (or a field of that type) fails the same way: `Unmarshal` and `Decode` return an error that names the type, because the adapter cannot fill it; decode into the adapter's `yaml.Node` instead. - **Node and comment round trips.** Decoding into a `yaml.Node` puts head, line and foot comments on the same nodes as yaml.v3: line comments, comments directly above a key or item, foot comments and document comments, and also a comment after a `---` marker, after a lone dash, after an anchor or tag on a collection or after an explicit key (`? k # c`), and comment lines between a key and its value. Comments inside a flow collection are read and written back as yaml.v3 does, and so is the foot comment of a root scalar or root flow collection. A comment line just before the closing bracket of a flow collection (`[a, b,` then `# note` then `]`) becomes the foot comment of the last item or key, as in yaml.v3. Encoding the node again writes the comments where yaml.v3 writes them, with these exceptions. When the last value of a flow mapping is empty (`{a: ,` then `# note` then `}`), yaml.v3 puts that comment on the mapping and the adapter on the key, so the adapter writes it back inside the braces and yaml.v3 after them. Where yaml.v3 writes text that does not read back as the same values, the adapter writes text that does: after a comment inside a flow collection that sits in a block collection, yaml.v3 can start a line with the closing bracket or a comma at a column where it no longer belongs to the collection, and the adapter indents it; and a folded scalar (`>`) is always written so that it reads back as the same text, where yaml.v3's text for some values, such as one that starts with a line break or has a line followed by a more indented line, reads back with a line break lost or added. If you diff files written this way, check one. Two comment placements still differ. A comment block that stands alone above a blank line, at the start of a sequence that is the value of a key (`k:` then `# a`, a blank line, `# b` and `- x`), is read as `k`'s foot comment, where yaml.v3 keeps it above the first item; a mapping in the same place matches yaml.v3. And when a foot comment is set on a mapping or sequence that is an item of a block sequence, the adapter writes it after that item, where yaml.v3 writes it at the end of the document. - **Invalid and unusual input.** Valid YAML that yaml.v3 refuses is read, invalid YAML that yaml.v3 accepts is refused, and inputs that make yaml.v3 panic return an error. Tests that pin yaml.v3's behaviour on such input will change. - **Numeric conversions.** A plain `3.5` read into an integer field is a type error instead of being truncated to 3; a float with no fraction (`3.0`, `1e3`) is accepted. In a sequence of numbers, a null leaves a zero in its place instead of being dropped. - **Limits.** Reading runs under the default limits of the `oneyaml` package, which yaml.v3 does not have, and there is no `Untrusted()` mode. Very deep or alias-heavy input that yaml.v3 read may now return an error. For input you do not control, read with the `oneyaml` package and `Untrusted()`. ## Move to the oneyaml API | yaml.v3 | oneyaml | |---|---| | `yaml.Unmarshal(data, &v)` | `oneyaml.Unmarshal(data, &v)` | | `yaml.Marshal(v)` | `oneyaml.Marshal(v)` | | `yaml.NewDecoder(r)`, `dec.Decode(&v)`, `io.EOF` at the end | `oneyaml.NewDecoder(r)`, the same | | `dec.KnownFields(true)` | `oneyaml.KnownFields()` passed to the call, or `oneyaml.KnownFields(b)` for a setting held in a variable; `errors.Is(err, oneyaml.ErrUnknownField)` matches a key the struct lacks | | `yaml.NewEncoder(w)`, `enc.SetIndent(n)` | `oneyaml.NewEncoder(w, oneyaml.Indent(n))` | | `yaml.Marshaler`, `MarshalYAML() (interface{}, error)` | `oneyaml.Marshaler`, the same method | | `yaml.Unmarshaler`, `UnmarshalYAML(*yaml.Node) error` | `oneyaml.Unmarshaler`, `UnmarshalYAML(oneyaml.Node) error`; the old form is an error, see [Hooks](#hooks) | | `UnmarshalYAML(func(interface{}) error) error` | the same method, unchanged | | `yaml.Node` for reading | `Document.Root()` and `oneyaml.Node`, a read-only view; `HeadComment()` and `LineComment()` return comments trimmed as yaml.v3 does | | `yaml.Node` as a field, to keep part of a document unparsed | `Document.Get(pointer, &node)`; a `oneyaml.Node` is not a decode target | | `yaml.Node` to change a file | `Document.Set`, `Remove`, `Rename`, `SetHeadComment`, `SetLineComment`, `ApplyPatch`; see [Editing a file](#editing-a-file) | | `yaml.TypeError` with `Errors []string` | `*oneyaml.TypeError` for the first value that does not fit, with the others in `More`; `All()` returns every one, each with its path and position | | struct tag `omitempty` | the same: zero values, empty collections and zero structs are left out, and an `IsZero` method decides when a type has one. One difference: a struct counts as zero only when all its fields are zero, unexported ones too, where yaml.v3 looks at exported fields only | | struct tags `flow` and `inline` (a struct or a map) | the same | | no `omitzero` | `omitzero` leaves out a zero value, or one whose `IsZero` returns true, as `encoding/json` does; an empty but non-nil slice is kept | | a field with no `yaml` tag is named by its lower-cased Go name | a `json` tag is used first, then the lower-cased Go name | `omitempty` and `omitzero` side by side: ```go type Resources struct { CPU string `yaml:"cpu"` Memory string `yaml:"memory"` } out, err := oneyaml.Marshal(struct { Name string `yaml:"name"` Resources Resources `yaml:"resources,omitempty"` Args []string `yaml:"args,omitempty"` Env []string `yaml:"env,omitzero"` }{Name: "api", Args: []string{}, Env: []string{}}) if err != nil { log.Fatal(err) } fmt.Print(string(out)) // Output: // name: api // env: [] ``` ## Behaviour that differs The `oneyaml` package follows YAML 1.2 with the Core schema. yaml.v3 follows a mix of 1.1 and 1.2. The differences you are likely to meet: - **`yes`, `no`, `on`, `off` are strings.** A bool field refuses them with a `*TypeError` that names the path and position. Write `true` and `false`, or use the compat package. ```go var cfg struct { Debug bool `yaml:"debug"` } err := oneyaml.Unmarshal([]byte("debug: yes\n"), &cfg) var te *oneyaml.TypeError if errors.As(err, &te) { fmt.Println(te.Path, te.Line, te.Column) } // Output: /debug 1 8 ``` - **`0755` is the integer 755**, not octal. `0o755` is octal. - **A date is a string** in a generic value; there is no implicit `time.Time`. - **The `<<` key is an ordinary key** unless you pass `MergeKeys()`. - **A nil slice is written as `null`**, not `[]`. The default indentation is two spaces, not four. - **Type errors are collected**, as in yaml.v3: a value that does not fit is skipped, the rest is read, and one `*TypeError` reports every mismatch; the read stops at the 100th, and fewer are reported when their paths are very long. A syntax error, a limit error or an error from your own hook stops the read at once and is returned alone. - **A duplicate key is an error**, as in yaml.v3, and more keys count as duplicates. Keys are compared by value, so `1` and `0x1` clash, which yaml.v3 allows. A key repeated inside a value the target ignores, such as `x: {y: 1, y: 2}` read into a struct with no field `x`, is refused too; yaml.v3 does not look there. The rules are in the [`Unmarshal` reference](https://pkg.go.dev/oneyaml.dev/go#Unmarshal). ```go src := []byte("mode: 0755\nwhen: 2001-02-03\nbase: &b {a: 1}\nuse:\n <<: *b\n") var plain, merged map[string]any if err := oneyaml.Unmarshal(src, &plain); err != nil { log.Fatal(err) } if err := oneyaml.Unmarshal(src, &merged, oneyaml.MergeKeys()); err != nil { log.Fatal(err) } fmt.Printf("%v %q\n", plain["mode"], plain["when"]) fmt.Println(plain["use"]) fmt.Println(merged["use"]) out, err := oneyaml.Marshal(struct{ Tags []string }{}) if err != nil { log.Fatal(err) } fmt.Print(string(out)) // Output: // 755 "2001-02-03" // map[<<:map[a:1]] // map[a:1] // tags: null ``` ## Hooks When it reads a value, the `oneyaml` package tries a type's methods in this order: `UnmarshalYAML(oneyaml.Node) error`, then `UnmarshalYAML(func(any) error) error`, then `UnmarshalText` for a scalar, then `UnmarshalJSON`. When it writes, the order is `MarshalYAML() (any, error)`, `MarshalText`, then `MarshalJSON`. For a type written for yaml.v3: | Method on your type | With the `oneyaml` package | |---|---| | `UnmarshalYAML(value *yaml.Node) error` | a `*TypeError` that names the type and the accepted forms. Change the parameter to `oneyaml.Node`, which has the same `Decode` method | | `UnmarshalYAML(unmarshal func(interface{}) error) error` | called as yaml.v3 calls it | | `MarshalYAML() (interface{}, error)` | called as yaml.v3 calls it | | `MarshalYAML` or `UnmarshalYAML` with any other signature | a `*TypeError` | | `UnmarshalText` and `MarshalText` | used for scalars, as in yaml.v3 | | `UnmarshalJSON` and `MarshalJSON`, and no YAML or text method | used, with the value passed as JSON text; yaml.v3 ignores them. Numbers arrive with their digits as written, on their own or inside a mapping or sequence; forms JSON cannot spell, such as `0x1F` or `.5`, are rewritten (`31`, `0.5`) | A method with a signature the package does not call is reported, not skipped, so a type is never quietly filled field by field when you meant it to read itself. The compat package keeps yaml.v3's own form: with the import swapped, `UnmarshalYAML(*yaml.Node) error` works unchanged in your own code. Hooks in other modules need the steps under [hooks you do not own](#check-after-the-swap). In the example, `legacySize` has yaml.v3's `UnmarshalYAML(n *yaml.Node) error` and `Size` is the same type moved over, with `UnmarshalYAML(n oneyaml.Node) error`. Both bodies are `return n.Decode(&s.Bytes)`. ```go src := []byte("limit: 512\n") var before struct { Limit legacySize `yaml:"limit"` } err := oneyaml.Unmarshal(src, &before) fmt.Println(err) var after struct { Limit Size `yaml:"limit"` } if err := oneyaml.Unmarshal(src, &after); err != nil { log.Fatal(err) } fmt.Println(after.Limit.Bytes) // Output: // oneyaml: line 1, column 8: /limit: oneyaml_test.legacySize has an UnmarshalYAML method that is neither UnmarshalYAML(oneyaml.Node) error nor UnmarshalYAML(func(any) error) error; change its signature, or remove it to fill the value by its fields // 512 ``` The older form needs no change. `Tags` here has `UnmarshalYAML(unmarshal func(any) error) error` and reads either a sequence or a comma-separated string: ```go var doc struct { A Tags `yaml:"a"` B Tags `yaml:"b"` } if err := oneyaml.Unmarshal([]byte("a: [x, y]\nb: \"p,q,r\"\n"), &doc); err != nil { log.Fatal(err) } fmt.Println(doc.A, doc.B) // Output: // [x y] [p q r] ``` ## Editing a file yaml.v3 can keep comments by loading into a `yaml.Node`, changing it and encoding the whole tree again, so the encoder decides the layout. With `Document` you set the value you mean and everything else stays as it was: ```go src := "# service settings\nname: api # public name\nreplicas: 2\n" doc, err := oneyaml.Parse([]byte(src)) if err != nil { log.Fatal(err) } if err := doc.Set("/replicas", 3); err != nil { log.Fatal(err) } fmt.Print(string(doc.Bytes())) // Output: // # service settings // name: api # public name // replicas: 3 ``` The same holds for the other changes yaml.v3 makes through a `yaml.Node`. To rename a key, call `Rename(pointer, newKey)` instead of setting the key node's `Value`. To change a comment, call `SetHeadComment` or `SetLineComment` instead of setting the node's `HeadComment` or `LineComment` field. Both edit the file in place, and the rest of it stays as it was. The rules for the style of the new value, for adding, removing and renaming keys, for comments and for anchors are in [Editing](https://recursiveai.org/oneyaml/go/docs/editing/). For how the libraries compare, see [Comparison](https://recursiveai.org/oneyaml/go/docs/comparison/). The supported Go versions are listed under [Stability](https://recursiveai.org/oneyaml/go/docs/overview/#stability) in the README. # Migrating from sigs.k8s.io/yaml Two ways to move from `sigs.k8s.io/yaml` (and its parent `ghodss/yaml`): keep your code and swap the import to the `compat/k8syaml` adapter, or move to the `oneyaml` API. sigs.k8s.io/yaml converts YAML to JSON and lets `encoding/json` bind it, so what it reads is what the JSON round trip can carry. The `oneyaml` package reads YAML directly. ## Swap the import ```sh # before: import "sigs.k8s.io/yaml" # after: import "oneyaml.dev/go/compat/k8syaml" go get oneyaml.dev/go@latest ``` The package is named `yaml`, as the original is, so call sites compile unchanged. It is a compatibility adapter with listed differences, not a copy. `Marshal`, `Unmarshal`, `UnmarshalStrict`, `YAMLToJSON`, `YAMLToJSONStrict`, `JSONToYAML`, `JSONOpt` and `DisallowUnknownFields` keep their names and signatures. The adapter does not provide `JSONObjectToYAMLObject`, which returns a type of the yaml.v2 module, or the `kyaml`, `yamlfmt`, `goyaml.v2` and `goyaml.v3` subpackages of sigs.k8s.io/yaml; code that uses them keeps its sigs.k8s.io/yaml import for those calls. Like sigs.k8s.io/yaml, it reads YAML 1.1 scalars (`yes`, `on` and `0755`), expands merge keys, reads the first document only, and binds through the `json` tags of your types: ```go var c Container err := yaml.Unmarshal([]byte(` name: web image: nginx:1.25 args: [--port, 8080, --verbose] env: DEBUG: yes RETRIES: 3 unknown: ignored `), &c) fmt.Println(err) fmt.Printf("%s %s %q %v\n", c.Name, c.Image, c.Args, c.Env) // Output: // // web nginx:1.25 ["--port" "8080" "--verbose"] map[DEBUG:true RETRIES:3] ``` `UnmarshalStrict` and `YAMLToJSONStrict` also reject duplicate keys and unknown fields: ```go var c Container err := yaml.UnmarshalStrict([]byte("name: web\nimage: nginx\nreplicas: 3\n"), &c) fmt.Println(err != nil) // Output: // true ``` Options for the JSON decoder work as before; this one keeps integers above 2^53 exact: ```go var v map[string]any useNumber := func(d *json.Decoder) *json.Decoder { d.UseNumber(); return d } if err := yaml.Unmarshal([]byte("id: 12345678901234567890\n"), &v, useNumber); err != nil { fmt.Println(err) } fmt.Println(v["id"]) // Output: // 12345678901234567890 ``` Marshal writes what sigs.k8s.io/yaml writes, with keys sorted: ```go out, err := yaml.Marshal(Container{ Name: "web", Image: "nginx:1.25", Args: []string{"--port", "8080"}, Env: map[string]string{"DEBUG": "yes", "RETRIES": "3"}, }) fmt.Println(err) fmt.Print(string(out)) // Output: // // args: // - --port // - "8080" // env: // DEBUG: "yes" // RETRIES: "3" // image: nginx:1.25 // name: web ``` It differs where sigs.k8s.io/yaml loses or changes data: a merge key never overrides a key written in the same mapping, a float read into a string field keeps its precision, `1` and `"1"` as keys are one JSON key with the last one winning, and YAML 1.2 syntax the reader behind sigs.k8s.io/yaml rejects is read. It returns an error where sigs.k8s.io/yaml panics on a nil target. The [package documentation](https://pkg.go.dev/oneyaml.dev/go/compat/k8syaml) lists every difference; check them against what your manifests rely on, in particular numbers read into string fields and keys that print the same. The limits are the default limits of the `oneyaml` package and cannot be changed, and there is no `Untrusted()` mode: for input you do not control, read with the `oneyaml` package. ## Move to the oneyaml API | sigs.k8s.io/yaml | oneyaml | |---|---| | `yaml.Unmarshal(y, &obj)` | `oneyaml.Unmarshal(y, &obj)` | | `yaml.UnmarshalStrict(y, &obj)` | `oneyaml.Unmarshal(y, &obj, oneyaml.KnownFields())`; `errors.Is(err, oneyaml.ErrUnknownField)` matches an unknown field, and duplicate keys are always an error | | `yaml.Marshal(obj)` | `oneyaml.Marshal(obj)` | | `yaml.YAMLToJSON(y)` | `oneyaml.Unmarshal` into `any`, then `json.Marshal` | | `yaml.JSONToYAML(j)` | `oneyaml.Unmarshal(j, &v)`, then `oneyaml.Marshal(v)` | | `JSONOpt` such as `UseNumber` | not needed: integers above 2^53 stay exact | ## Behaviour that differs - **Tags.** sigs.k8s.io/yaml reads and writes `json` tags and ignores `yaml` tags. The `oneyaml` package uses a field's `yaml` tag and falls back to its `json` tag only when the field has no `yaml` tag. A field that has both reads differently after the move. Types written for Kubernetes carry only `json` tags and work as they are: ```go var c struct { Name string `json:"name"` Image string `json:"image,omitempty"` } if err := oneyaml.Unmarshal([]byte("name: web\nimage: nginx:1.25\n"), &c); err != nil { log.Fatal(err) } fmt.Println(c.Name, c.Image) // Output: web nginx:1.25 ``` - **Field order.** A round trip through JSON sorts struct fields. `Marshal` keeps their declaration order and sorts only map keys. ```go type Container struct { Name string `json:"name"` Image string `json:"image"` Args []string `json:"args,omitempty"` } out, err := oneyaml.Marshal(Container{Name: "web", Image: "nginx:1.25", Args: []string{"--port", "8080"}}) if err != nil { log.Fatal(err) } fmt.Print(string(out)) // Output: // name: web // image: nginx:1.25 // args: // - --port // - '8080' ``` - **YAML 1.2, not 1.1.** `yes` and `on` are strings and `0755` is the integer 755. A bool field refuses `yes`, and the error names the path and position: ```go var cfg struct { Debug bool `yaml:"debug"` } err := oneyaml.Unmarshal([]byte("debug: yes\n"), &cfg) var te *oneyaml.TypeError if errors.As(err, &te) { fmt.Println(te.Path, te.Line, te.Column) } // Output: /debug 1 8 ``` - **Every type error is reported.** sigs.k8s.io/yaml binds through `encoding/json`, which skips a value that does not fit and reports only the earliest. The `oneyaml` package also skips it and reads the rest, but its `*TypeError` lists every mismatch in `More` and `All()`; the read stops at the 100th, and fewer are reported when their paths are very long. A syntax error, a limit error or an error from your own hook stops the read at once and is returned alone. - **Merge keys are off** unless you pass `MergeKeys()`. See [Migrating from yaml.v3](https://recursiveai.org/oneyaml/go/docs/migrate-yaml-v3/) for the other 1.1 forms. - **Keys keep their type.** Into `any`, a mapping with a non-string key is a `map[any]any`; sigs.k8s.io/yaml turns every key into a string. - **JSON methods are used, after the YAML and text ones.** A type with `UnmarshalJSON` or `MarshalJSON` and no YAML or text method is read and written through them, with the value passed as JSON text, so types written for `encoding/json`, such as Kubernetes API types, need no change. A value that JSON cannot hold, such as a mapping with a key that is not a string, or `.nan`, is a `*TypeError` for such a type. Numbers arrive with their digits as written, on their own or inside a mapping or sequence, so a quantity such as `12345678901234567890.5` is not rounded. The order differs from `encoding/json`: a YAML method (`UnmarshalYAML`, `MarshalYAML`) ranks first, and for a scalar `UnmarshalText` and `MarshalText` rank above the JSON methods, where sigs.k8s.io/yaml calls the JSON methods first. `Level` here has only `UnmarshalJSON` and `MarshalJSON`: ```go var cfg struct { Level Level `yaml:"level"` } if err := oneyaml.Unmarshal([]byte("level: high\n"), &cfg); err != nil { log.Fatal(err) } fmt.Println(cfg.Level) out, err := oneyaml.Marshal(cfg) if err != nil { log.Fatal(err) } fmt.Print(string(out)) // The error from UnmarshalJSON comes back inside a *TypeError. err = oneyaml.Unmarshal([]byte("level: loud\n"), &cfg) fmt.Println(err) // Output: // 2 // level: high // oneyaml: line 1, column 8: /level: cannot unmarshal "loud" into oneyaml_test.Level: unknown level "loud" ``` - **JSON is YAML.** A JSON document reads as it is, and integers above 2^53 stay exact: ```go var v map[string]any if err := oneyaml.Unmarshal([]byte(`{"id": 12345678901234567890, "ok": true}`), &v); err != nil { log.Fatal(err) } fmt.Println(v["id"], v["ok"]) // Output: 12345678901234567890 true ``` The supported Go versions are listed under [Stability](https://recursiveai.org/oneyaml/go/docs/overview/#stability) in the README. For how the libraries compare, see [Comparison](https://recursiveai.org/oneyaml/go/docs/comparison/). # Comparison How oneyaml-go compares with the Go YAML libraries it is most often weighed against: `gopkg.in/yaml.v3` (and its maintained fork `go.yaml.in/yaml/v3`), its prerelease successor `go.yaml.in/yaml/v4`, `sigs.k8s.io/yaml` and `github.com/goccy/go-yaml`. **Bias.** This page is written by the authors of oneyaml-go. We chose the criteria, and they are the ones oneyaml-go 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. Where a statement rests on a source, the source is named; library status was surveyed on 2026-09-30 (yaml/v4 on 2026-10-15) and changes. This page compares features and behaviour. Time and memory are measured on [Performance](https://recursiveai.org/oneyaml/go/docs/performance/), from a full benchmark run against these libraries; its method line names the library commit measured and the date, and the page says which release that commit is. That page has the evidence, and this one does not repeat it. ## What each library is | | oneyaml-go | yaml.v3 | yaml/v4 | sigs.k8s.io/yaml | goccy/go-yaml | |---|---|---|---|---|---| | Approach | own scanner, one parser feeding a decoder and a lossless document | own port of libyaml, `Node` tree | the yaml.v3 code base, `Node` tree, `Load`/`Dump` with options | converts YAML to JSON, `encoding/json` binds | own parser with an AST | | Default YAML version | 1.2, Core schema | a mix of 1.1 and 1.2 | a mix of 1.1 and 1.2 | 1.1 | a mix | | Status | new | original repo archived in 2025; `go.yaml.in/yaml/v3` maintained, security fixes only | prerelease: `v4.0.0-rc.6` (2026-06-17) was the newest on 2026-10-15; where new work on go-yaml happens | maintained by the Kubernetes project | maintained | | Dependencies | none outside the standard library | none | none | yaml.v2 fork | none | ## Like with like Compare libraries on the job they share. - **Typed load and dump** (`Unmarshal`, `Marshal`, decoders and encoders): all five. yaml/v4 adds `Load` and `Dump` with functional options, as oneyaml-go takes options on its calls. oneyaml-go reads YAML 1.2 forms where yaml.v3 and sigs.k8s.io/yaml read many 1.1 forms; the compat packages ([yaml.v3](https://recursiveai.org/oneyaml/go/docs/migrate-yaml-v3/), [sigs.k8s.io/yaml](https://recursiveai.org/oneyaml/go/docs/migrate-sigs-k8s-yaml/)) bring the 1.1 forms back. - **Editing a file and keeping it as it was:** yaml.v3 and yaml/v4 (`Node`, which encodes the whole tree again) and goccy/go-yaml (AST and YAML Path) can keep comments; sigs.k8s.io/yaml cannot, because the JSON round trip has no comments. The yaml/v4 documentation says of its `Node` that "the content when re-encoded will not have its original textual representation preserved" ([`Node`](https://pkg.go.dev/go.yaml.in/yaml/v4@v4.0.0-rc.6#Node)). oneyaml-go's `Document` keeps every byte it does not change; it does not re-encode the tree. - **Limits on input:** oneyaml-go bounds depth, alias expansion, number size and patch work by default and has an `Untrusted()` preset ([Limits](https://recursiveai.org/oneyaml/go/docs/limits/)). yaml/v4 has a limit plugin (`WithPlugin` with `plugin/limit`) that checks nesting depth and alias expansion, configured per call. - **Patching:** oneyaml-go applies RFC 6902 and 7396 to YAML, with `test` comparing values by the YAML data model rather than as JSON values ([details](https://recursiveai.org/oneyaml/go/docs/patches/#the-test-operation)). None of the other four has a patch API; third-party packages build one on top of them. - **Kubernetes manifests:** sigs.k8s.io/yaml is the reference for how a manifest binds to a type with `json` tags. `compat/k8syaml` reproduces it. - **Benchmarks:** the benchmark times a library on a task only after it checks the library's answer. On the document tasks, a library counts only where its document wrote the input back byte for byte, so on many inputs yaml.v3, yaml/v4 and goccy/go-yaml have no number. Those rows [have no rival](https://recursiveai.org/oneyaml/go/docs/performance/#rows-with-no-rival) and are not wins. ## Where oneyaml-go is the better fit - You change YAML files that people also edit by hand, and the diff must contain only your change. As an illustration, in the project's own checks on the two block-scalar cases of [Editing](https://recursiveai.org/oneyaml/go/docs/editing/), yaml.v3 kept the block style for both but lost the blank line before the next comment, and goccy/go-yaml kept the first case byte for byte but wrote a plain scalar for the second unless the caller supplied the style. Two cases show the approach; they do not measure it. - You need YAML 1.2 with the Core schema, checked against the yaml-test-suite, with the known deviations listed ([Conformance](https://recursiveai.org/oneyaml/go/docs/conformance/)). - You read input from outside, and want bounds on nesting, alias expansion and number size on by default, and a tighter preset in one option that also bounds input size. See [Limits](https://recursiveai.org/oneyaml/go/docs/limits/) and what they do not cover. - You want RFC 6902 or 7396 patches on a YAML file. - Time and memory matter for loading, writing or editing. [Performance](https://recursiveai.org/oneyaml/go/docs/performance/) compares each workload with the strongest competitor that gave a correct answer, shows the verdict for each, and explains why the unchanged-emit row is [not a rendering speed](https://recursiveai.org/oneyaml/go/docs/performance/#unchanged-emit). ## Where the others are the better fit **yaml.v3** - It is the incumbent. Tens of thousands of modules import it, and your dependencies, linters and tools probably already do. Staying on it costs nothing. - Its `Node` API is mature and widely documented; code and examples for it are everywhere. - Bug-for-bug compatibility with data it already wrote. The compat package reproduces its documented behaviour, but the original is the reference. **yaml/v4** - It continues the yaml.v3 code base under the YAML organization, so code and knowledge from yaml.v3 carry over, and an option preset (`WithV3Defaults`) selects yaml.v3-compatible defaults. - Its maintainers say all new go-yaml work happens in v4. It is a prerelease today; check its status when you choose. **sigs.k8s.io/yaml** - It is the Kubernetes reference. A manifest means what sigs.k8s.io/yaml says it means. The `oneyaml` package calls `UnmarshalJSON` and `MarshalJSON` too, but ranks YAML and text methods above them and reads YAML 1.2 scalars, so a type with several hooks, or a manifest that relies on YAML 1.1 forms, can read differently. The compat package follows the original, but the original remains the authority. **goccy/go-yaml** - It exposes an AST and YAML Path expressions for filtering and replacing. `oneyaml` does not offer queries; its pointers address one value. - In the project's own checks it passes more of the yaml-test-suite than yaml.v3 does. **All four** - They are older and have more field experience. oneyaml-go is new: its releases are listed in the [changelog](https://recursiveai.org/oneyaml/go/docs/changelog/), and it has little use in the field so far. ## Choosing | If you | Use | |---|---| | Edit YAML files that humans maintain | oneyaml-go | | Apply JSON Patch or merge patch to YAML | oneyaml-go | | Read untrusted input | oneyaml-go with `Untrusted()` | | Have a working yaml.v3 or sigs.k8s.io/yaml program and no reason to change | keep it, or swap in the compat package to gain its parser and limits | | Need AST queries and YAML Path | goccy/go-yaml | | Need to match Kubernetes behaviour exactly | sigs.k8s.io/yaml | Corrections are welcome; open an issue with the behaviour and a reproduction. # Performance This page compares the time and memory of oneyaml-go with those of the other Go YAML libraries. Every library runs the same tasks on the same inputs, through the benchmark of [oneyaml-kit](https://github.com/recursive-ai-oss/oneyaml-kit), the kit shared by the oneyaml libraries. The tables under [Results](#results) are generated from the kit's result file, and none of their numbers is typed by hand. The summary at the top of the [README](https://recursiveai.org/oneyaml/go/docs/overview/) is generated from the same file. The figures are measured, not bounds. They describe these inputs on this machine, and your files and hardware will give other numbers. ## About this run - **Library:** oneyaml-go 1.0.0. The figures are for the code released as `v1.0.0`, at commit `eea67bb`; the tag sits on a later commit that changes no library code. This page is generated again from a new run at each release. - **Competitors:** `go.yaml.in/yaml/v3`, the maintained fork of yaml.v3 (`yaml-v3` in the tables), `go.yaml.in/yaml/v4` (`yaml-v4`), `gopkg.in/yaml.v2` (`yaml-v2`), `sigs.k8s.io/yaml` (`sigs-yaml`) and `github.com/goccy/go-yaml` (`goccy-go-yaml`). For patches, they are `github.com/palantir/pkg/yamlpatch` with its yaml.v3 back end (`palantir-yamlpatch`) and with its goccy/go-yaml back end (`palantir-yamlpatch-goccy`), and `github.com/evanphx/json-patch/v5` on top of sigs.k8s.io/yaml (`json-patch`). The method line under each table gives the exact versions. - **Machine and date:** a full run on 2026-10-16, on a shared workstation, not a reserved machine. Other work ran on it at the same time, and the method line under each table records the host's load before the run, after it and at its highest during it. The cells run one after another, so a busy moment slows only the cells measured during it, ours or a competitor's; weigh a close result with that in mind. The method line also gives the CPU, the operating system and the Go version. - **Kit:** revision [`0abba1d`](https://github.com/recursive-ai-oss/oneyaml-kit/tree/0abba1d8b4f2088881710b5835c24bf9eca149b9). The kit's [benchmark notes](https://github.com/recursive-ai-oss/oneyaml-kit/blob/0abba1d8b4f2088881710b5835c24bf9eca149b9/docs/benchmarks.md) at that revision describe the method in full. This page summarises them. ## What is measured ### Tasks Each task starts from a state that is built outside the clock, fresh for every iteration, with one exception. In unchanged emit, a write can take nanoseconds, so a library whose write of a used document allocates the same as a write of a fresh one shares one parsed document between iterations. oneyaml-go's write allocates nothing either way. The timed work is what a caller of the library does. | Task | Timed work, as oneyaml-go does it | The answer is accepted when | |---|---|---| | typed load | `Unmarshal` into Go structs | the structs, written as JSON, equal the expected value | | generic load | `Unmarshal` into `any`, or a `Decoder` for every document of a stream | the value, written as JSON, equals the expected value | | dump | `Marshal` of the library's own value tree, which was read from the input | two independent reference parsers read the text back to the expected value | | document parse | `Parse` into a `Document` that keeps comments, spacing and styles | the document holds the expected value and writes the input back byte for byte | | unchanged emit | `Document.Bytes` on a document that was not edited | the bytes equal the input | | edit script | a fixed list of replacements and removals with `Document.Set` and `Document.Remove`, then `Document.Bytes` | the bytes equal the expected bytes exactly: only the edited values change, each in its own style | | patch (RFC 6902) | `Parse`, `Document.Apply` with a JSON Patch, then `Document.Bytes` | the bytes equal the expected bytes, and a second patch with a failing step leaves the text unchanged and returns an error | The competitors do the same work through their own APIs. For the document tasks that means yaml.v3's and yaml/v4's `Node` and goccy/go-yaml's AST. In the Go harness, every library's patch is timed from text to text: read the file, apply the patch, and write the text back. A library that cannot do a task at all, such as a library with no lossless document, gets an "unsupported" cell, which is not a loss. ### Workloads The inputs are real files, vendored byte for byte with their source and licence: a GitHub Actions workflow, a Compose file, Helm chart values, two OpenAPI descriptions, a multi-document Kubernetes manifest and a Prometheus configuration. Generated inputs add typed records, Compose files with anchors and merge keys, Kubernetes streams, OpenAPI and Helm-style documents and a flat wide mapping at several sizes, and stress shapes: deep nesting, long scalars of every style, flow-heavy data, anchors and aliases, Unicode, and every scalar style and chomping mode. Two small reference cases edit a block scalar. The kit's notes at the run's revision list them under [Workloads](https://github.com/recursive-ai-oss/oneyaml-kit/blob/0abba1d8b4f2088881710b5835c24bf9eca149b9/docs/benchmarks.md#workloads). The expected values do not come from any of the libraries being measured. A generated input and its value come from the same generator. A real file's value is accepted only when two independent YAML 1.2 parsers agree on it. The expected bytes after an edit are exact text substitutions on the input. ### The correctness check before timing Each library's answer is checked before it is timed. A library gets a number only if its answer was right. A wrong answer is marked "differs" and a refused input is marked "rejects", and neither carries a time or a memory figure. The report of the run lists every such cell with the first difference the check found. See the [correctness guard](https://github.com/recursive-ai-oss/oneyaml-kit/blob/0abba1d8b4f2088881710b5835c24bf9eca149b9/docs/benchmarks.md#correctness-guard) in the kit's notes at the run's revision. ### Time and memory The time is the ns/op that Go's `testing.Benchmark` reports, so it is a mean over the iterations of the benchmark. In a full run each cell runs for Go's standard benchtime of one second. The memory figures are the bytes and allocations per operation that `testing.B` reports with `ReportAllocs`. They count what was allocated during the timed work, not the peak size of the heap. ### Reading the tables Each row compares oneyaml-go with the strongest competitor in that row: the fastest for time, and the one that allocated least for memory. The ratio is our figure divided by theirs, so below 1.0× is better for oneyaml-go. A small gap counts as a tie (the kit's [verdicts](https://github.com/recursive-ai-oss/oneyaml-kit/blob/0abba1d8b4f2088881710b5835c24bf9eca149b9/docs/benchmarks.md#verdicts) at the run's revision give the bands: under 5% for time and 2% for memory), and a loss is marked ▼. A note in parentheses after a time names the answer a competitor was checked against when it is not the plain one. *(merged)* means it applied `<<` merge keys while loading, a YAML 1.1 extension, and was checked against the merged value; YAML 1.2, and oneyaml-go by default, read `<<` as an ordinary key. Such a cell did different work from ours and is still compared with. *(value-only)* is explained by the † below. A † marks a cell that was not measured on exactly the task as specified, for example json-patch, which works on JSON and so cannot keep the text of the file. It is still compared with. In this Go run every patch cell carries a † for "text API", ours included: every library's patch is timed from the input text to the output text, and the palantir-yamlpatch cells passed the same byte-for-byte check as ours, so that mark does not mean less work. Only the "value-only" json-patch cells were judged more loosely. So on a patch cell in this run, a † says how the cell was measured, not that it did less work. The README summary takes, for each task, the geometric mean over the real-world files that task has. It states the ratio the other way round, as how many times faster oneyaml-go was: the competitor's time divided by ours, so higher is better there. The tables below show every workload. ## Unchanged emit The unchanged-emit ratio is much further below 1.0× than that of any other row, and it does not measure how fast oneyaml-go renders YAML. A `Document` keeps the text it parsed. `Bytes` on a document that was not edited returns that text without copying it, so its cost does not grow with the size of the file. The competitors build their text again from a `Node` tree or an AST. The task measures what a caller pays to write back a file they did not change, which is a real use, but the gap comes from not rendering, not from rendering faster. For the time it takes to write YAML from values, see the dump row. For the time an edit takes, including writing the result, see the edit-script row. ## Rows with no rival A row says "no rival" when oneyaml-go gave a correct answer and no competitor did. There is no comparison in such a row, so it is not a win and it is not counted as one. In this run those rows are: - **document parse, unchanged emit and edit script** on several real files and generated inputs: the competitors' documents did not write the input back byte for byte. They changed blank lines, indentation, quoting or escapes, or the layout of a flow collection. In the edit script on the `reference-b` case, goccy/go-yaml also wrote the edited block scalar as a plain scalar, so the text lost its style. - **dump** on the typed-records inputs: the text the competitors wrote did not read back to the expected value. For example, a float with a whole-number value came back as an integer, or a string of digits lost its quotes and came back as a number. - **patch** on the GitHub Actions workflow: the competitors' output differed from the expected bytes, or the patch was refused. The report of the run gives the first difference for each of these cells. ## Reproducing the run From a checkout of [oneyaml-kit](https://github.com/recursive-ai-oss/oneyaml-kit) at the revision above, with `KIT_ONEYAML_GO_SRC` set to a checkout of this repository at `v1.0.0`: ```sh bin/bench run --mode full --lang go bin/bench readme --results out --library oneyaml-go --readme /README.md --docs /docs --charts docs/img ``` The first command writes the result file, a report with every cell and charts under `out/bench`. The second writes the generated blocks of this page and of the README, and the README's charts. The blocks on this page were written by `bin/bench readme` at the run's kit revision, from that result file. A full run is a result wherever it runs, and its method line says what kind of machine it ran on and, for a local machine, the host's load. A quick run, or a run started with `--smoke`, is labelled a smoke result. It shows the layout and the rough picture but it is not a claim, and `bin/bench readme` refuses it unless you pass `--allow-smoke`, which labels every block. ## Results **Time per operation** | Task | Workload | Input | `oneyaml-go` | `yaml-v3` | `yaml-v4` | `yaml-v2` | `sigs-yaml` | `goccy-go-yaml` | `palantir-yamlpatch` | `palantir-yamlpatch-goccy` | `json-patch` | Ours vs strongest competitor | |---|---|---|---|---|---|---|---|---|---|---|---|---| | typed load | `gha-workflow` | 9.43 KiB | 121 µs | 301 µs | 310 µs | 234 µs | differs [13] | 935 µs | unsupported | unsupported | unsupported | 0.52× of `yaml-v2` ▲ win | | typed load | `records-4k` | 4.14 KiB | 104 µs | 259 µs | 251 µs | 196 µs | 376 µs | 619 µs | unsupported | unsupported | unsupported | 0.53× of `yaml-v2` ▲ win | | typed load | `records-64k` | 64.2 KiB | 1.44 ms | 5.11 ms | 4.89 ms | 3.31 ms | 5.95 ms | 10.3 ms | unsupported | unsupported | unsupported | 0.44× of `yaml-v2` ▲ win | | typed load | `records-1m` | 1 MiB | 23.7 ms | 69.5 ms | 70.2 ms | 55.9 ms | 93.7 ms | 166 ms | unsupported | unsupported | unsupported | 0.42× of `yaml-v2` ▲ win | | typed load | `records-8m` | 8 MiB | 155 ms | 507 ms | 487 ms | 395 ms | 666 ms | 1.4 s | unsupported | unsupported | unsupported | 0.39× of `yaml-v2` ▲ win | | generic load | `compose-wordpress` | 815 B | 7.06 µs | 27.3 µs | 28.1 µs | 21.3 µs | differs [16] | 40.7 µs | unsupported | unsupported | unsupported | 0.33× of `yaml-v2` ▲ win | | generic load | `gha-workflow` | 9.43 KiB | 105 µs | 334 µs | 371 µs | differs [18] | differs [13] | 747 µs | unsupported | unsupported | unsupported | 0.31× of `yaml-v3` ▲ win | | generic load | `helm-ingress-nginx` | 52.4 KiB | 168 µs | 988 µs | 942 µs | 555 µs | differs [16] | 1.54 ms | unsupported | unsupported | unsupported | 0.30× of `yaml-v2` ▲ win | | generic load | `k8s-ingress-nginx-multi` | 16 KiB | 263 µs | 682 µs | 636 µs | 565 µs | differs [15] | 1.13 ms | unsupported | unsupported | unsupported | 0.47× of `yaml-v2` ▲ win | | generic load | `openapi-api-with-examples` | 6.75 KiB | 44.5 µs | 146 µs | 150 µs | 126 µs | 182 µs | 252 µs | unsupported | unsupported | unsupported | 0.35× of `yaml-v2` ▲ win | | generic load | `openapi-petstore` | 5.34 KiB | 58.3 µs | 164 µs | 173 µs | 140 µs | 250 µs | 454 µs | unsupported | unsupported | unsupported | 0.42× of `yaml-v2` ▲ win | | generic load | `prometheus-config` | 1.1 KiB | 7.05 µs | 32.4 µs | 32.8 µs | 20.2 µs | 27.3 µs | 45 µs | unsupported | unsupported | unsupported | 0.35× of `yaml-v2` ▲ win | | generic load | `reference-a` | 51 B | 1.23 µs | 6.38 µs | 6.79 µs | 4.57 µs | differs [16] | 7.37 µs | unsupported | unsupported | unsupported | 0.27× of `yaml-v2` ▲ win | | generic load | `reference-b` | 51 B | 1.32 µs | 6.85 µs | 6.76 µs | 5.06 µs | differs [16] | 9.99 µs | unsupported | unsupported | unsupported | 0.26× of `yaml-v2` ▲ win | | generic load | `records-4k` | 4.14 KiB | 72.4 µs | 289 µs | 270 µs | 216 µs | differs [16] | 441 µs | unsupported | unsupported | unsupported | 0.34× of `yaml-v2` ▲ win | | generic load | `records-64k` | 64.2 KiB | 1.31 ms | 5.36 ms | 5.39 ms | 4.46 ms | differs [16] | 10.4 ms | unsupported | unsupported | unsupported | 0.29× of `yaml-v2` ▲ win | | generic load | `records-1m` | 1 MiB | 19.6 ms | 74.2 ms | 74.6 ms | 62.9 ms | differs [16] | 138 ms | unsupported | unsupported | unsupported | 0.31× of `yaml-v2` ▲ win | | generic load | `records-8m` | 8 MiB | 140 ms | 538 ms | 531 ms | 441 ms | differs [16] | 1.21 s | unsupported | unsupported | unsupported | 0.32× of `yaml-v2` ▲ win | | generic load | `compose-merge-2k` | 2.14 KiB | 57.4 µs | 178 µs (merged) | 180 µs (merged) | 140 µs (merged) | differs [16] | 280 µs (merged) | unsupported | unsupported | unsupported | 0.41× of `yaml-v2` ▲ win | | generic load | `compose-merge-64k` | 64.1 KiB | 1.65 ms | 5.01 ms (merged) | 5.51 ms (merged) | 4.18 ms (merged) | differs [16] | 10.4 ms (merged) | unsupported | unsupported | unsupported | 0.40× of `yaml-v2` ▲ win | | generic load | `compose-merge-1m` | 1 MiB | 24.4 ms | 105 ms (merged) | 112 ms (merged) | 53.6 ms (merged) | differs [16] | 166 ms (merged) | unsupported | unsupported | unsupported | 0.46× of `yaml-v2` ▲ win | | generic load | `k8s-multi-16k` | 16.4 KiB | 451 µs | 1.28 ms | 1.23 ms | 836 µs | differs [15] | 2.7 ms | unsupported | unsupported | unsupported | 0.54× of `yaml-v2` ▲ win | | generic load | `k8s-multi-1m` | 1 MiB | 22.8 ms | 57.4 ms | 62.8 ms | 47.3 ms | differs [15] | 138 ms | unsupported | unsupported | unsupported | 0.48× of `yaml-v2` ▲ win | | generic load | `openapi-100k` | 101 KiB | 1.79 ms | 4.92 ms | 5.99 ms | 4.26 ms | 7.2 ms | 12.7 ms | unsupported | unsupported | unsupported | 0.42× of `yaml-v2` ▲ win | | generic load | `openapi-8m` | 8 MiB | 113 ms | 722 ms | 733 ms | 283 ms | 457 ms | 1.12 s | unsupported | unsupported | unsupported | 0.40× of `yaml-v2` ▲ win | | generic load | `helm-values-64k` | 64 KiB | 775 µs | 3.35 ms | 3.07 ms | 1.75 ms | differs [16] | 6.84 ms | unsupported | unsupported | unsupported | 0.44× of `yaml-v2` ▲ win | | generic load | `helm-values-1m` | 1 MiB | 10.8 ms | 56.5 ms | 58.5 ms | 32.4 ms | differs [16] | 102 ms | unsupported | unsupported | unsupported | 0.33× of `yaml-v2` ▲ win | | generic load | `deep-block` | 5.23 KiB | 45 µs | 148 µs | 154 µs | 116 µs | differs [16] | 299 µs | unsupported | unsupported | unsupported | 0.39× of `yaml-v2` ▲ win | | generic load | `deep-flow` | 202 B | 16 µs | 94.5 µs | 91.8 µs | 84.4 µs | differs [16] | 113 µs | unsupported | unsupported | unsupported | 0.19× of `yaml-v2` ▲ win | | generic load | `wide-map-64k` | 64 KiB | 1.25 ms | 18.6 ms | 19.6 ms | 4.02 ms | differs [16] | 27.3 ms | unsupported | unsupported | unsupported | 0.31× of `yaml-v2` ▲ win | | generic load | `wide-map-1m` | 1 MiB | 16.6 ms | 3.63 s | 3.69 s | 51.2 ms | differs [16] | 1.98 s | unsupported | unsupported | unsupported | 0.32× of `yaml-v2` ▲ win | | generic load | `long-scalars-256k` | 261 KiB | 663 µs | 3.59 ms | 3.51 ms | 3.37 ms | 5.45 ms | 11.8 ms | unsupported | unsupported | unsupported | 0.20× of `yaml-v2` ▲ win | | generic load | `flow-heavy-64k` | 64 KiB | 2.39 ms | 8.05 ms | 8.11 ms | 6.3 ms | differs [16] | 17 ms | unsupported | unsupported | unsupported | 0.38× of `yaml-v2` ▲ win | | generic load | `anchors-32k` | 32 KiB | 2.92 ms | 6.08 ms | 6.37 ms | differs [19] | differs [16] | 6.62 ms | unsupported | unsupported | unsupported | 0.48× of `yaml-v3` ▲ win | | generic load | `unicode-32k` | 32 KiB | 272 µs | 995 µs | 950 µs | 896 µs | 1.24 ms | 2.11 ms | unsupported | unsupported | unsupported | 0.30× of `yaml-v2` ▲ win | | generic load | `scalar-styles-32k` | 32 KiB | 405 µs | 1.45 ms | 1.45 ms | 1.16 ms | 1.74 ms | 4.48 ms | unsupported | unsupported | unsupported | 0.35× of `yaml-v2` ▲ win | | dump | `compose-wordpress` | 815 B | 1.86 µs | 23.3 µs | 34.6 µs | 22.9 µs | 57.3 µs | 42 µs | unsupported | unsupported | unsupported | 0.08× of `yaml-v2` ▲ win | | dump | `gha-workflow` | 9.43 KiB | 29.5 µs | 332 µs | 306 µs | differs [18] | differs [13] | 680 µs | unsupported | unsupported | unsupported | 0.10× of `yaml-v4` ▲ win | | dump | `helm-ingress-nginx` | 52.4 KiB | 42.2 µs | 511 µs | 587 µs | 479 µs | 871 µs | 940 µs | unsupported | unsupported | unsupported | 0.09× of `yaml-v2` ▲ win | | dump | `openapi-api-with-examples` | 6.75 KiB | 12.7 µs | 192 µs | 202 µs | 159 µs | 323 µs | 324 µs | unsupported | unsupported | unsupported | 0.08× of `yaml-v2` ▲ win | | dump | `openapi-petstore` | 5.34 KiB | 19.6 µs | 193 µs | 194 µs | 188 µs | 413 µs | 391 µs | unsupported | unsupported | unsupported | 0.10× of `yaml-v2` ▲ win | | dump | `prometheus-config` | 1.1 KiB | 1.27 µs | 26.1 µs | 29.7 µs | 21.9 µs | 48.1 µs | 32.5 µs | unsupported | unsupported | unsupported | 0.06× of `yaml-v2` ▲ win | | dump | `reference-a` | 51 B | 222 ns | 3.55 µs | 4.71 µs | 2.85 µs | 9.28 µs | 7.51 µs | unsupported | unsupported | unsupported | 0.08× of `yaml-v2` ▲ win | | dump | `reference-b` | 51 B | 214 ns | 3.56 µs | 4.88 µs | 2.9 µs | 8.95 µs | 5.72 µs | unsupported | unsupported | unsupported | 0.07× of `yaml-v2` ▲ win | | dump | `records-4k` | 4.14 KiB | 19.4 µs | differs [22] | differs [24] | differs [20] | differs [17] | differs [3] | unsupported | unsupported | unsupported | no rival | | dump | `records-64k` | 64.2 KiB | 319 µs | differs [22] | differs [24] | differs [20] | differs [17] | differs [3] | unsupported | unsupported | unsupported | no rival | | dump | `records-1m` | 1 MiB | 6.89 ms | differs [22] | differs [24] | differs [20] | differs [17] | differs [3] | unsupported | unsupported | unsupported | no rival | | dump | `records-8m` | 8 MiB | 52.9 ms | differs [22] | differs [24] | differs [20] | differs [17] | differs [3] | unsupported | unsupported | unsupported | no rival | | dump | `compose-merge-2k` | 2.14 KiB | 16.9 µs | 220 µs (merged) | 250 µs (merged) | 190 µs (merged) | 395 µs (merged) | 467 µs (merged) | unsupported | unsupported | unsupported | 0.09× of `yaml-v2` ▲ win | | dump | `compose-merge-64k` | 64.1 KiB | 619 µs | 6.47 ms (merged) | 6.85 ms (merged) | 5.92 ms (merged) | 9.29 ms (merged) | 11.3 ms (merged) | unsupported | unsupported | unsupported | 0.10× of `yaml-v2` ▲ win | | dump | `compose-merge-1m` | 1 MiB | 14.2 ms | 137 ms (merged) | 132 ms (merged) | 114 ms (merged) | 175 ms (merged) | 201 ms (merged) | unsupported | unsupported | unsupported | 0.13× of `yaml-v2` ▲ win | | dump | `openapi-100k` | 101 KiB | 518 µs | 4.85 ms | 5.22 ms | 4.25 ms | 7.51 ms | 8.72 ms | unsupported | unsupported | unsupported | 0.12× of `yaml-v2` ▲ win | | dump | `openapi-8m` | 8 MiB | 57.9 ms | 655 ms | 745 ms | 556 ms | 867 ms | 888 ms | unsupported | unsupported | unsupported | 0.10× of `yaml-v2` ▲ win | | dump | `helm-values-64k` | 64 KiB | 192 µs | 2.11 ms | 2.33 ms | 2.04 ms | 3.58 ms | 4.17 ms | unsupported | unsupported | unsupported | 0.09× of `yaml-v2` ▲ win | | dump | `helm-values-1m` | 1 MiB | 3.24 ms | 40 ms | 45.4 ms | 36.5 ms | 59.4 ms | 71.6 ms | unsupported | unsupported | unsupported | 0.09× of `yaml-v2` ▲ win | | dump | `deep-block` | 5.23 KiB | 14.1 µs | 150 µs | 145 µs | 120 µs | 243 µs | 389 µs | unsupported | unsupported | unsupported | 0.12× of `yaml-v2` ▲ win | | dump | `deep-flow` | 202 B | 1.32 µs | 83 µs | 91.5 µs | 53.2 µs | 190 µs | 73.7 µs | unsupported | unsupported | unsupported | 0.02× of `yaml-v2` ▲ win | | dump | `wide-map-64k` | 64 KiB | 542 µs | 4.95 ms | 5.49 ms | 5 ms | 8.19 ms | 9.66 ms | unsupported | unsupported | unsupported | 0.11× of `yaml-v3` ▲ win | | dump | `wide-map-1m` | 1 MiB | 11.9 ms | 120 ms | 113 ms | 109 ms | 179 ms | 174 ms | unsupported | unsupported | unsupported | 0.11× of `yaml-v2` ▲ win | | dump | `long-scalars-256k` | 261 KiB | 704 µs | 2.94 ms | 2.93 ms | 2.88 ms | 6.19 ms | 1.09 ms | unsupported | unsupported | unsupported | 0.64× of `goccy-go-yaml` ▲ win | | dump | `flow-heavy-64k` | 64 KiB | 347 µs | 6.04 ms | 6.9 ms | 5.24 ms | 10.8 ms | 11.3 ms | unsupported | unsupported | unsupported | 0.07× of `yaml-v2` ▲ win | | dump | `anchors-32k` | 32 KiB | 855 µs | 11.5 ms | 13.2 ms | differs [19] | differs [14] | 24.4 ms | unsupported | unsupported | unsupported | 0.07× of `yaml-v3` ▲ win | | dump | `unicode-32k` | 32 KiB | 57.3 µs | 832 µs | 934 µs | 715 µs | 1.28 ms | 1.32 ms | unsupported | unsupported | unsupported | 0.08× of `yaml-v2` ▲ win | | dump | `scalar-styles-32k` | 32 KiB | 108 µs | 1.39 ms | 1.6 ms | 1.31 ms | 2.12 ms | 3.45 ms | unsupported | unsupported | unsupported | 0.08× of `yaml-v2` ▲ win | | document parse | `compose-wordpress` | 815 B | 5.88 µs | differs [21] | differs [23] | unsupported | unsupported | differs [2] | unsupported | unsupported | unsupported | no rival | | document parse | `gha-workflow` | 9.43 KiB | 63.8 µs | differs [21] | differs [23] | unsupported | unsupported | differs [2] | unsupported | unsupported | unsupported | no rival | | document parse | `helm-ingress-nginx` | 52.4 KiB | 133 µs | 844 µs | differs [23] | unsupported | unsupported | 1.61 ms | unsupported | unsupported | unsupported | 0.16× of `yaml-v3` ▲ win | | document parse | `k8s-ingress-nginx-multi` | 16 KiB | 146 µs | differs [21] | 545 µs | unsupported | unsupported | 1.03 ms | unsupported | unsupported | unsupported | 0.27× of `yaml-v4` ▲ win | | document parse | `openapi-api-with-examples` | 6.75 KiB | 34 µs | differs [21] | differs [23] | unsupported | unsupported | differs [2] | unsupported | unsupported | unsupported | no rival | | document parse | `openapi-petstore` | 5.34 KiB | 33.3 µs | 155 µs | differs [23] | unsupported | unsupported | 380 µs | unsupported | unsupported | unsupported | 0.21× of `yaml-v3` ▲ win | | document parse | `prometheus-config` | 1.1 KiB | 5.59 µs | differs [21] | differs [23] | unsupported | unsupported | differs [2] | unsupported | unsupported | unsupported | no rival | | document parse | `reference-a` | 51 B | 1.48 µs | differs [21] | differs [23] | unsupported | unsupported | 8.22 µs | unsupported | unsupported | unsupported | 0.18× of `goccy-go-yaml` ▲ win | | document parse | `reference-b` | 51 B | 1.55 µs | differs [21] | differs [23] | unsupported | unsupported | 7.99 µs | unsupported | unsupported | unsupported | 0.19× of `goccy-go-yaml` ▲ win | | document parse | `records-4k` | 4.14 KiB | 50.9 µs | 248 µs | differs [23] | unsupported | unsupported | differs [5] | unsupported | unsupported | unsupported | 0.21× of `yaml-v3` ▲ win | | document parse | `records-64k` | 64.2 KiB | 719 µs | 3.74 ms | differs [23] | unsupported | unsupported | differs [5] | unsupported | unsupported | unsupported | 0.19× of `yaml-v3` ▲ win | | document parse | `records-1m` | 1 MiB | 10.8 ms | 52.3 ms | differs [23] | unsupported | unsupported | differs [5] | unsupported | unsupported | unsupported | 0.21× of `yaml-v3` ▲ win | | document parse | `records-8m` | 8 MiB | 88.3 ms | 400 ms | differs [23] | unsupported | unsupported | differs [5] | unsupported | unsupported | unsupported | 0.22× of `yaml-v3` ▲ win | | document parse | `compose-merge-2k` | 2.14 KiB | 25.6 µs | differs [21] | 105 µs (merged) | unsupported | unsupported | 236 µs (merged) | unsupported | unsupported | unsupported | 0.24× of `yaml-v4` ▲ win | | document parse | `compose-merge-64k` | 64.1 KiB | 665 µs | differs [21] | 3 ms (merged) | unsupported | unsupported | 7.58 ms (merged) | unsupported | unsupported | unsupported | 0.22× of `yaml-v4` ▲ win | | document parse | `compose-merge-1m` | 1 MiB | 9.28 ms | differs [21] | 38.4 ms (merged) | unsupported | unsupported | 131 ms (merged) | unsupported | unsupported | unsupported | 0.24× of `yaml-v4` ▲ win | | document parse | `k8s-multi-16k` | 16.4 KiB | 216 µs | differs [21] | differs [23] | unsupported | unsupported | 2.26 ms | unsupported | unsupported | unsupported | 0.10× of `goccy-go-yaml` ▲ win | | document parse | `k8s-multi-1m` | 1 MiB | 12 ms | differs [21] | differs [23] | unsupported | unsupported | 116 ms | unsupported | unsupported | unsupported | 0.10× of `goccy-go-yaml` ▲ win | | document parse | `openapi-100k` | 101 KiB | 970 µs | 4.26 ms | differs [23] | unsupported | unsupported | 9.24 ms | unsupported | unsupported | unsupported | 0.23× of `yaml-v3` ▲ win | | document parse | `openapi-8m` | 8 MiB | 72.3 ms | 262 ms | differs [23] | unsupported | unsupported | 851 ms | unsupported | unsupported | unsupported | 0.28× of `yaml-v3` ▲ win | | document parse | `helm-values-64k` | 64 KiB | 475 µs | differs [21] | differs [23] | unsupported | unsupported | differs [2] | unsupported | unsupported | unsupported | no rival | | document parse | `helm-values-1m` | 1 MiB | 6.76 ms | differs [21] | differs [23] | unsupported | unsupported | differs [2] | unsupported | unsupported | unsupported | no rival | | document parse | `deep-block` | 5.23 KiB | 29.9 µs | differs [21] | differs [23] | unsupported | unsupported | 202 µs | unsupported | unsupported | unsupported | 0.15× of `goccy-go-yaml` ▲ win | | document parse | `deep-flow` | 202 B | 21 µs | 68.9 µs | differs [23] | unsupported | unsupported | 88.3 µs | unsupported | unsupported | unsupported | 0.30× of `yaml-v3` ▲ win | | document parse | `wide-map-64k` | 64 KiB | 650 µs | 3.69 ms | 3.33 ms | unsupported | unsupported | 24.1 ms | unsupported | unsupported | unsupported | 0.20× of `yaml-v4` ▲ win | | document parse | `wide-map-1m` | 1 MiB | 8.67 ms | 48.8 ms | 44.8 ms | unsupported | unsupported | 1.9 s | unsupported | unsupported | unsupported | 0.19× of `yaml-v4` ▲ win | | document parse | `long-scalars-256k` | 261 KiB | 401 µs | differs [21] | differs [23] | unsupported | unsupported | 11.4 ms | unsupported | unsupported | unsupported | 0.04× of `goccy-go-yaml` ▲ win | | document parse | `flow-heavy-64k` | 64 KiB | 1.38 ms | 5.44 ms | 5.72 ms | unsupported | unsupported | 14.5 ms | unsupported | unsupported | unsupported | 0.25× of `yaml-v3` ▲ win | | document parse | `anchors-32k` | 32 KiB | 445 µs | 1.87 ms | differs [23] | unsupported | unsupported | 6.15 ms | unsupported | unsupported | unsupported | 0.24× of `yaml-v3` ▲ win | | document parse | `unicode-32k` | 32 KiB | 190 µs | differs [21] | differs [23] | unsupported | unsupported | differs [4] | unsupported | unsupported | unsupported | no rival | | document parse | `scalar-styles-32k` | 32 KiB | 244 µs | differs [21] | differs [23] | unsupported | unsupported | 4.01 ms | unsupported | unsupported | unsupported | 0.06× of `goccy-go-yaml` ▲ win | | unchanged emit | `compose-wordpress` | 815 B | 4.24 ns | differs [21] | differs [23] | unsupported | unsupported | differs [2] | unsupported | unsupported | unsupported | no rival | | unchanged emit | `gha-workflow` | 9.43 KiB | 4.47 ns | differs [21] | differs [23] | unsupported | unsupported | differs [2] | unsupported | unsupported | unsupported | no rival | | unchanged emit | `helm-ingress-nginx` | 52.4 KiB | 4.32 ns | 511 µs | differs [23] | unsupported | unsupported | 412 µs | unsupported | unsupported | unsupported | <0.01× of `goccy-go-yaml` ▲ win | | unchanged emit | `k8s-ingress-nginx-multi` | 16 KiB | 4.3 ns | differs [21] | 617 µs | unsupported | unsupported | 263 µs | unsupported | unsupported | unsupported | <0.01× of `goccy-go-yaml` ▲ win | | unchanged emit | `openapi-api-with-examples` | 6.75 KiB | 4.31 ns | differs [21] | differs [23] | unsupported | unsupported | differs [2] | unsupported | unsupported | unsupported | no rival | | unchanged emit | `openapi-petstore` | 5.34 KiB | 4.32 ns | 151 µs | differs [23] | unsupported | unsupported | 59.4 µs | unsupported | unsupported | unsupported | <0.01× of `goccy-go-yaml` ▲ win | | unchanged emit | `prometheus-config` | 1.1 KiB | 4.36 ns | differs [21] | differs [23] | unsupported | unsupported | differs [2] | unsupported | unsupported | unsupported | no rival | | unchanged emit | `reference-a` | 51 B | 4.64 ns | differs [21] | differs [23] | unsupported | unsupported | 1.12 µs | unsupported | unsupported | unsupported | <0.01× of `goccy-go-yaml` ▲ win | | unchanged emit | `reference-b` | 51 B | 4.34 ns | differs [21] | differs [23] | unsupported | unsupported | 1.13 µs | unsupported | unsupported | unsupported | <0.01× of `goccy-go-yaml` ▲ win | | unchanged emit | `records-4k` | 4.14 KiB | 4.4 ns | 255 µs | differs [23] | unsupported | unsupported | differs [5] | unsupported | unsupported | unsupported | <0.01× of `yaml-v3` ▲ win | | unchanged emit | `records-64k` | 64.2 KiB | 4.42 ns | 4.04 ms | differs [23] | unsupported | unsupported | differs [5] | unsupported | unsupported | unsupported | <0.01× of `yaml-v3` ▲ win | | unchanged emit | `records-1m` | 1 MiB | 4.31 ns | 55.6 ms | differs [23] | unsupported | unsupported | differs [5] | unsupported | unsupported | unsupported | <0.01× of `yaml-v3` ▲ win | | unchanged emit | `records-8m` | 8 MiB | 4.31 ns | 594 ms | differs [23] | unsupported | unsupported | differs [5] | unsupported | unsupported | unsupported | <0.01× of `yaml-v3` ▲ win | | unchanged emit | `compose-merge-2k` | 2.14 KiB | 4.44 ns | differs [21] | 93 µs | unsupported | unsupported | 33.6 µs | unsupported | unsupported | unsupported | <0.01× of `goccy-go-yaml` ▲ win | | unchanged emit | `compose-merge-64k` | 64.1 KiB | 4.64 ns | differs [21] | 2.77 ms | unsupported | unsupported | 785 µs | unsupported | unsupported | unsupported | <0.01× of `goccy-go-yaml` ▲ win | | unchanged emit | `compose-merge-1m` | 1 MiB | 4.38 ns | differs [21] | 51.3 ms | unsupported | unsupported | 16.2 ms | unsupported | unsupported | unsupported | <0.01× of `goccy-go-yaml` ▲ win | | unchanged emit | `k8s-multi-16k` | 16.4 KiB | 4.42 ns | differs [21] | differs [23] | unsupported | unsupported | 389 µs | unsupported | unsupported | unsupported | <0.01× of `goccy-go-yaml` ▲ win | | unchanged emit | `k8s-multi-1m` | 1 MiB | 4.66 ns | differs [21] | differs [23] | unsupported | unsupported | 20.5 ms | unsupported | unsupported | unsupported | <0.01× of `goccy-go-yaml` ▲ win | | unchanged emit | `openapi-100k` | 101 KiB | 4.54 ns | 5.04 ms | differs [23] | unsupported | unsupported | 1.68 ms | unsupported | unsupported | unsupported | <0.01× of `goccy-go-yaml` ▲ win | | unchanged emit | `openapi-8m` | 8 MiB | 4.3 ns | 600 ms | differs [23] | unsupported | unsupported | 155 ms | unsupported | unsupported | unsupported | <0.01× of `goccy-go-yaml` ▲ win | | unchanged emit | `helm-values-64k` | 64 KiB | 4.48 ns | differs [21] | differs [23] | unsupported | unsupported | differs [2] | unsupported | unsupported | unsupported | no rival | | unchanged emit | `helm-values-1m` | 1 MiB | 4.51 ns | differs [21] | differs [23] | unsupported | unsupported | differs [2] | unsupported | unsupported | unsupported | no rival | | unchanged emit | `deep-block` | 5.23 KiB | 4.51 ns | differs [21] | differs [23] | unsupported | unsupported | 139 µs | unsupported | unsupported | unsupported | <0.01× of `goccy-go-yaml` ▲ win | | unchanged emit | `deep-flow` | 202 B | 4.65 ns | 54.5 µs | differs [23] | unsupported | unsupported | 12.1 µs | unsupported | unsupported | unsupported | <0.01× of `goccy-go-yaml` ▲ win | | unchanged emit | `wide-map-64k` | 64 KiB | 4.63 ns | 4.35 ms | 2.81 ms | unsupported | unsupported | 766 µs | unsupported | unsupported | unsupported | <0.01× of `goccy-go-yaml` ▲ win | | unchanged emit | `wide-map-1m` | 1 MiB | 4.44 ns | 49.9 ms | 49.2 ms | unsupported | unsupported | 10 ms | unsupported | unsupported | unsupported | <0.01× of `goccy-go-yaml` ▲ win | | unchanged emit | `long-scalars-256k` | 261 KiB | 4.6 ns | differs [21] | differs [23] | unsupported | unsupported | 233 µs | unsupported | unsupported | unsupported | <0.01× of `goccy-go-yaml` ▲ win | | unchanged emit | `flow-heavy-64k` | 64 KiB | 4.69 ns | 9.21 ms | 5.33 ms | unsupported | unsupported | 1.63 ms | unsupported | unsupported | unsupported | <0.01× of `goccy-go-yaml` ▲ win | | unchanged emit | `anchors-32k` | 32 KiB | 4.69 ns | 1.68 ms | differs [23] | unsupported | unsupported | 905 µs | unsupported | unsupported | unsupported | <0.01× of `goccy-go-yaml` ▲ win | | unchanged emit | `unicode-32k` | 32 KiB | 4.61 ns | differs [21] | differs [23] | unsupported | unsupported | differs [4] | unsupported | unsupported | unsupported | no rival | | unchanged emit | `scalar-styles-32k` | 32 KiB | 4.61 ns | differs [21] | differs [23] | unsupported | unsupported | 771 µs | unsupported | unsupported | unsupported | <0.01× of `goccy-go-yaml` ▲ win | | edit script | `compose-wordpress` | 815 B | 8.68 µs | differs [21] | differs [23] | unsupported | unsupported | differs [2] | unsupported | unsupported | unsupported | no rival | | edit script | `gha-workflow` | 9.43 KiB | 10.5 µs | differs [21] | differs [23] | unsupported | unsupported | differs [2] | unsupported | unsupported | unsupported | no rival | | edit script | `helm-ingress-nginx` | 52.4 KiB | 11.3 µs | 570 µs | differs [23] | unsupported | unsupported | 580 µs | unsupported | unsupported | unsupported | 0.02× of `yaml-v3` ▲ win | | edit script | `openapi-api-with-examples` | 6.75 KiB | 1.98 µs | differs [21] | differs [23] | unsupported | unsupported | differs [2] | unsupported | unsupported | unsupported | no rival | | edit script | `openapi-petstore` | 5.34 KiB | 16.6 µs | 141 µs | differs [23] | unsupported | unsupported | differs [2] | unsupported | unsupported | unsupported | 0.12× of `yaml-v3` ▲ win | | edit script | `prometheus-config` | 1.1 KiB | 1.76 µs | differs [21] | differs [23] | unsupported | unsupported | differs [2] | unsupported | unsupported | unsupported | no rival | | edit script | `reference-a` | 51 B | 2.7 µs | differs [21] | differs [23] | unsupported | unsupported | 7.1 µs | unsupported | unsupported | unsupported | 0.38× of `goccy-go-yaml` ▲ win | | edit script | `reference-b` | 51 B | 2.23 µs | differs [21] | differs [23] | unsupported | unsupported | differs [1] | unsupported | unsupported | unsupported | no rival | | edit script | `records-4k` | 4.14 KiB | 7.49 µs | 235 µs | differs [23] | unsupported | unsupported | differs [5] | unsupported | unsupported | unsupported | 0.03× of `yaml-v3` ▲ win | | edit script | `records-64k` | 64.2 KiB | 36.3 µs | 3.4 ms | differs [23] | unsupported | unsupported | differs [5] | unsupported | unsupported | unsupported | 0.01× of `yaml-v3` ▲ win | | edit script | `records-1m` | 1 MiB | 701 µs | 61.5 ms | differs [23] | unsupported | unsupported | differs [5] | unsupported | unsupported | unsupported | 0.01× of `yaml-v3` ▲ win | | edit script | `records-8m` | 8 MiB | 6.35 ms | 643 ms | differs [23] | unsupported | unsupported | differs [5] | unsupported | unsupported | unsupported | <0.01× of `yaml-v3` ▲ win | | edit script | `compose-merge-2k` | 2.14 KiB | 6.93 µs | differs [21] | 100 µs | unsupported | unsupported | 46.3 µs | unsupported | unsupported | unsupported | 0.15× of `goccy-go-yaml` ▲ win | | edit script | `compose-merge-64k` | 64.1 KiB | 40.6 µs | differs [21] | 2.71 ms | unsupported | unsupported | 1.26 ms | unsupported | unsupported | unsupported | 0.03× of `goccy-go-yaml` ▲ win | | edit script | `compose-merge-1m` | 1 MiB | 605 µs | differs [21] | 50.6 ms | unsupported | unsupported | 17.4 ms | unsupported | unsupported | unsupported | 0.03× of `goccy-go-yaml` ▲ win | | edit script | `openapi-100k` | 101 KiB | 51.4 µs | 3.86 ms | differs [23] | unsupported | unsupported | 2.02 ms | unsupported | unsupported | unsupported | 0.03× of `goccy-go-yaml` ▲ win | | edit script | `openapi-8m` | 8 MiB | 5 ms | 547 ms | differs [23] | unsupported | unsupported | 180 ms | unsupported | unsupported | unsupported | 0.03× of `goccy-go-yaml` ▲ win | | edit script | `helm-values-64k` | 64 KiB | 19.9 µs | differs [21] | differs [23] | unsupported | unsupported | differs [2] | unsupported | unsupported | unsupported | no rival | | edit script | `helm-values-1m` | 1 MiB | 317 µs | differs [21] | differs [23] | unsupported | unsupported | differs [2] | unsupported | unsupported | unsupported | no rival | | edit script | `wide-map-64k` | 64 KiB | 57.3 µs | 2.99 ms | 2.83 ms | unsupported | unsupported | 827 µs | unsupported | unsupported | unsupported | 0.07× of `goccy-go-yaml` ▲ win | | edit script | `wide-map-1m` | 1 MiB | 962 µs | 51.2 ms | 48.1 ms | unsupported | unsupported | 12.4 ms | unsupported | unsupported | unsupported | 0.08× of `goccy-go-yaml` ▲ win | | patch (RFC 6902) | `compose-wordpress` | 815 B | 15.7 µs † | unsupported | unsupported | unsupported | unsupported | unsupported | differs [9] | differs [11] | 92.1 µs (value-only) † | 0.17× of `json-patch` † (text API, value-only) ▲ win | | patch (RFC 6902) | `gha-workflow` | 9.43 KiB | 77.2 µs † | unsupported | unsupported | unsupported | unsupported | unsupported | differs [9] | differs [11] | rejects [6] | no rival | | patch (RFC 6902) | `reference-a` | 51 B | 4.29 µs † | unsupported | unsupported | unsupported | unsupported | unsupported | differs [9] | 12.4 µs † | 17.8 µs (value-only) † | 0.35× of `palantir-yamlpatch-goccy` † (text API) ▲ win | | patch (RFC 6902) | `reference-b` | 51 B | 3.45 µs † | unsupported | unsupported | unsupported | unsupported | unsupported | differs [8] | differs [10] | 16.3 µs (value-only) † | 0.21× of `json-patch` † (text API, value-only) ▲ win | | patch (RFC 6902) | `records-4k` | 4.14 KiB | 58.9 µs † | unsupported | unsupported | unsupported | unsupported | unsupported | 582 µs † | differs [12] | differs [7] | 0.10× of `palantir-yamlpatch` † (text API) ▲ win | | patch (RFC 6902) | `records-64k` | 64.2 KiB | 820 µs † | unsupported | unsupported | unsupported | unsupported | unsupported | 9.05 ms † | differs [12] | differs [7] | 0.09× of `palantir-yamlpatch` † (text API) ▲ win | | patch (RFC 6902) | `records-1m` | 1 MiB | 12.7 ms † | unsupported | unsupported | unsupported | unsupported | unsupported | 131 ms † | differs [12] | differs [7] | 0.10× of `palantir-yamlpatch` † (text API) ▲ win | | patch (RFC 6902) | `records-8m` | 8 MiB | 96 ms † | unsupported | unsupported | unsupported | unsupported | unsupported | 870 ms † | differs [12] | differs [7] | 0.11× of `palantir-yamlpatch` † (text API) ▲ win | **Allocated bytes per operation (B/op)** | Task | Workload | Input | `oneyaml-go` | `yaml-v3` | `yaml-v4` | `yaml-v2` | `sigs-yaml` | `goccy-go-yaml` | `palantir-yamlpatch` | `palantir-yamlpatch-goccy` | `json-patch` | Ours vs strongest competitor | |---|---|---|---|---|---|---|---|---|---|---|---|---| | typed load | `gha-workflow` | 9.43 KiB | 81.3 KiB in 1,238 allocs | 195 KiB in 3,908 allocs | 196 KiB in 4,027 allocs | 144 KiB in 3,242 allocs | differs [13] | 796 KiB in 14,521 allocs | unsupported | unsupported | unsupported | 0.57× of `yaml-v2` ▲ win | | typed load | `records-4k` | 4.14 KiB | 58.8 KiB in 788 allocs | 150 KiB in 3,360 allocs | 151 KiB in 3,436 allocs | 114 KiB in 2,715 allocs | 209 KiB in 4,615 allocs | 585 KiB in 11,573 allocs | unsupported | unsupported | unsupported | 0.52× of `yaml-v2` ▲ win | | typed load | `records-64k` | 64.2 KiB | 842 KiB in 11,916 allocs | 2.15 MiB in 51,238 allocs | 2.17 MiB in 52,358 allocs | 1.64 MiB in 41,182 allocs | 3.02 MiB in 70,076 allocs | 9.38 MiB in 177,038 allocs | unsupported | unsupported | unsupported | 0.50× of `yaml-v2` ▲ win | | typed load | `records-1m` | 1 MiB | 13.8 MiB in 187,204 allocs | 33.7 MiB in 812,226 allocs | 34 MiB in 829,755 allocs | 25.6 MiB in 650,361 allocs | 48.5 MiB in 1,103,395 allocs | 147 MiB in 2,790,106 allocs | unsupported | unsupported | unsupported | 0.54× of `yaml-v2` ▲ win | | typed load | `records-8m` | 8 MiB | 116 MiB in 1,479,205 allocs | 267 MiB in 6,422,920 allocs | 269 MiB in 6,561,501 allocs | 203 MiB in 5,141,571 allocs | 391 MiB in 8,722,305 allocs | 1.17 GiB in 22,057,681 allocs | unsupported | unsupported | unsupported | 0.57× of `yaml-v2` ▲ win | | generic load | `compose-wordpress` | 815 B | 2.69 KiB in 55 allocs | 20.9 KiB in 341 allocs | 21.2 KiB in 354 allocs | 15.8 KiB in 295 allocs | differs [16] | 50.8 KiB in 931 allocs | unsupported | unsupported | unsupported | 0.17× of `yaml-v2` ▲ win | | generic load | `gha-workflow` | 9.43 KiB | 45 KiB in 645 allocs | 216 KiB in 4,249 allocs | 218 KiB in 4,368 allocs | differs [18] | differs [13] | 750 KiB in 12,692 allocs | unsupported | unsupported | unsupported | 0.21× of `yaml-v3` ▲ win | | generic load | `helm-ingress-nginx` | 52.4 KiB | 41.4 KiB in 419 allocs | 559 KiB in 6,234 allocs | 562 KiB in 6,422 allocs | 218 KiB in 4,234 allocs | differs [16] | 1.7 MiB in 24,155 allocs | unsupported | unsupported | unsupported | 0.19× of `yaml-v2` ▲ win | | generic load | `k8s-ingress-nginx-multi` | 16 KiB | 202 KiB in 1,380 allocs | 403 KiB in 8,670 allocs | 403 KiB in 8,791 allocs | 335 KiB in 7,603 allocs | differs [15] | 1.37 MiB in 25,154 allocs | unsupported | unsupported | unsupported | 0.60× of `yaml-v2` ▲ win | | generic load | `openapi-api-with-examples` | 6.75 KiB | 20.9 KiB in 259 allocs | 88 KiB in 1,475 allocs | 125 KiB in 1,540 allocs | 75.4 KiB in 1,371 allocs | 133 KiB in 2,158 allocs | 349 KiB in 5,219 allocs | unsupported | unsupported | unsupported | 0.28× of `yaml-v2` ▲ win | | generic load | `openapi-petstore` | 5.34 KiB | 28.7 KiB in 313 allocs | 120 KiB in 2,123 allocs | 122 KiB in 2,208 allocs | 102 KiB in 1,842 allocs | 189 KiB in 2,858 allocs | 453 KiB in 7,441 allocs | unsupported | unsupported | unsupported | 0.28× of `yaml-v2` ▲ win | | generic load | `prometheus-config` | 1.1 KiB | 3.06 KiB in 39 allocs | 27.6 KiB in 319 allocs | 27.8 KiB in 334 allocs | 16 KiB in 240 allocs | 24.6 KiB in 369 allocs | 51.3 KiB in 825 allocs | unsupported | unsupported | unsupported | 0.19× of `yaml-v2` ▲ win | | generic load | `reference-a` | 51 B | 481 B in 7 allocs | 8.75 KiB in 74 allocs | 8.85 KiB in 78 allocs | 6.2 KiB in 61 allocs | differs [16] | 9.42 KiB in 213 allocs | unsupported | unsupported | unsupported | 0.08× of `yaml-v2` ▲ win | | generic load | `reference-b` | 51 B | 481 B in 7 allocs | 8.75 KiB in 74 allocs | 8.85 KiB in 78 allocs | 6.2 KiB in 61 allocs | differs [16] | 9.42 KiB in 213 allocs | unsupported | unsupported | unsupported | 0.08× of `yaml-v2` ▲ win | | generic load | `records-4k` | 4.14 KiB | 22.8 KiB in 439 allocs | 170 KiB in 3,896 allocs | 171 KiB in 3,972 allocs | 134 KiB in 3,251 allocs | differs [16] | 558 KiB in 10,787 allocs | unsupported | unsupported | unsupported | 0.17× of `yaml-v2` ▲ win | | generic load | `records-64k` | 64.2 KiB | 351 KiB in 6,629 allocs | 2.46 MiB in 59,521 allocs | 2.48 MiB in 60,641 allocs | 1.95 MiB in 49,465 allocs | differs [16] | 8.96 MiB in 164,851 allocs | unsupported | unsupported | unsupported | 0.18× of `yaml-v2` ▲ win | | generic load | `records-1m` | 1 MiB | 5.42 MiB in 107,985 allocs | 38.6 MiB in 942,220 allocs | 38.9 MiB in 959,750 allocs | 30.6 MiB in 780,356 allocs | differs [16] | 141 MiB in 2,598,696 allocs | unsupported | unsupported | unsupported | 0.18× of `yaml-v2` ▲ win | | generic load | `records-8m` | 8 MiB | 42.8 MiB in 854,968 allocs | 306 MiB in 7,450,690 allocs | 308 MiB in 7,589,271 allocs | 242 MiB in 6,169,342 allocs | differs [16] | 1.12 GiB in 20,544,552 allocs | unsupported | unsupported | unsupported | 0.18× of `yaml-v2` ▲ win | | generic load | `compose-merge-2k` | 2.14 KiB | 28 KiB in 426 allocs | 99.6 KiB in 2,154 allocs | 100 KiB in 2,206 allocs | 76.7 KiB in 1,768 allocs | differs [16] | 303 KiB in 5,763 allocs | unsupported | unsupported | unsupported | 0.37× of `yaml-v2` ▲ win | | generic load | `compose-merge-64k` | 64.1 KiB | 826 KiB in 12,559 allocs | 2.67 MiB in 62,157 allocs | 2.69 MiB in 63,557 allocs | 2.04 MiB in 50,650 allocs | differs [16] | 9.32 MiB in 164,901 allocs | unsupported | unsupported | unsupported | 0.40× of `yaml-v2` ▲ win | | generic load | `compose-merge-1m` | 1 MiB | 12.7 MiB in 200,789 allocs | 41.5 MiB in 965,903 allocs | 41.8 MiB in 987,612 allocs | 31.7 MiB in 786,856 allocs | differs [16] | 266 MiB in 2,561,566 allocs | unsupported | unsupported | unsupported | 0.40× of `yaml-v2` ▲ win | | generic load | `k8s-multi-16k` | 16.4 KiB | 305 KiB in 2,281 allocs | 704 KiB in 14,216 allocs | 703 KiB in 14,634 allocs | 576 KiB in 12,387 allocs | differs [15] | 2.36 MiB in 41,912 allocs | unsupported | unsupported | unsupported | 0.53× of `yaml-v2` ▲ win | | generic load | `k8s-multi-1m` | 1 MiB | 10.8 MiB in 139,019 allocs | 41.6 MiB in 869,070 allocs | 41.6 MiB in 894,808 allocs | 34.2 MiB in 757,097 allocs | differs [15] | 210 MiB in 2,566,830 allocs | unsupported | unsupported | unsupported | 0.32× of `yaml-v2` ▲ win | | generic load | `openapi-100k` | 101 KiB | 801 KiB in 9,676 allocs | 3.03 MiB in 61,776 allocs | 3.07 MiB in 64,197 allocs | 2.47 MiB in 52,998 allocs | 4.93 MiB in 83,621 allocs | 11.5 MiB in 183,059 allocs | unsupported | unsupported | unsupported | 0.32× of `yaml-v2` ▲ win | | generic load | `openapi-8m` | 8 MiB | 63.1 MiB in 828,253 allocs | 243 MiB in 4,958,983 allocs | 246 MiB in 5,153,246 allocs | 198 MiB in 4,253,511 allocs | 388 MiB in 6,707,707 allocs | 1.74 GiB in 14,714,040 allocs | unsupported | unsupported | unsupported | 0.32× of `yaml-v2` ▲ win | | generic load | `helm-values-64k` | 64 KiB | 235 KiB in 2,658 allocs | 1.94 MiB in 29,222 allocs | 1.95 MiB in 30,172 allocs | 1.05 MiB in 22,957 allocs | differs [16] | 5.87 MiB in 96,855 allocs | unsupported | unsupported | unsupported | 0.22× of `yaml-v2` ▲ win | | generic load | `helm-values-1m` | 1 MiB | 3.65 MiB in 44,988 allocs | 32.6 MiB in 461,687 allocs | 32.9 MiB in 476,677 allocs | 16.6 MiB in 362,749 allocs | differs [16] | 131 MiB in 1,532,043 allocs | unsupported | unsupported | unsupported | 0.22× of `yaml-v2` ▲ win | | generic load | `deep-block` | 5.23 KiB | 33.3 KiB in 205 allocs | 133 KiB in 1,165 allocs | 134 KiB in 1,269 allocs | 120 KiB in 963 allocs | differs [16] | 285 KiB in 3,384 allocs | unsupported | unsupported | unsupported | 0.28× of `yaml-v2` ▲ win | | generic load | `deep-flow` | 202 B | 4.03 KiB in 203 allocs | 146 KiB in 862 allocs | 145 KiB in 957 allocs | 141 KiB in 862 allocs | differs [16] | 126 KiB in 2,364 allocs | unsupported | unsupported | unsupported | 0.03× of `goccy-go-yaml` ▲ win | | generic load | `wide-map-64k` | 64 KiB | 403 KiB in 7,429 allocs | 2.73 MiB in 51,538 allocs | 2.74 MiB in 51,542 allocs | 2.25 MiB in 43,507 allocs | differs [16] | 64.4 MiB in 156,570 allocs | unsupported | unsupported | unsupported | 0.17× of `yaml-v2` ▲ win | | generic load | `wide-map-1m` | 1 MiB | 4.04 MiB in 108,125 allocs | 37.4 MiB in 747,437 allocs | 37.4 MiB in 747,432 allocs | 30.3 MiB in 630,438 allocs | differs [16] | 11.2 GiB in 2,269,268 allocs | unsupported | unsupported | unsupported | 0.13× of `yaml-v2` ▲ win | | generic load | `long-scalars-256k` | 261 KiB | 267 KiB in 15 allocs | 2.19 MiB in 26,140 allocs | 2.19 MiB in 26,144 allocs | 2.19 MiB in 26,132 allocs | 4.74 MiB in 26,212 allocs | 10.2 MiB in 127,457 allocs | unsupported | unsupported | unsupported | 0.12× of `yaml-v2` ▲ win | | generic load | `flow-heavy-64k` | 64 KiB | 562 KiB in 13,496 allocs | 3.84 MiB in 90,055 allocs | 3.88 MiB in 93,319 allocs | 3.06 MiB in 79,825 allocs | differs [16] | 17 MiB in 294,329 allocs | unsupported | unsupported | unsupported | 0.18× of `yaml-v2` ▲ win | | generic load | `anchors-32k` | 32 KiB | 1.87 MiB in 21,130 allocs | 3.54 MiB in 79,510 allocs | 3.55 MiB in 80,153 allocs | differs [19] | differs [16] | 8.41 MiB in 135,861 allocs | unsupported | unsupported | unsupported | 0.53× of `yaml-v3` ▲ win | | generic load | `unicode-32k` | 32 KiB | 71.9 KiB in 2,905 allocs | 491 KiB in 11,644 allocs | 491 KiB in 11,648 allocs | 450 KiB in 11,079 allocs | 685 KiB in 14,012 allocs | 1.92 MiB in 35,519 allocs | unsupported | unsupported | unsupported | 0.16× of `yaml-v2` ▲ win | | generic load | `scalar-styles-32k` | 32 KiB | 92.3 KiB in 2,661 allocs | 715 KiB in 15,981 allocs | 718 KiB in 16,151 allocs | 575 KiB in 14,486 allocs | 955 KiB in 21,822 allocs | 3.71 MiB in 71,476 allocs | unsupported | unsupported | unsupported | 0.16× of `yaml-v2` ▲ win | | dump | `compose-wordpress` | 815 B | 713 B in 1 allocs | 39.7 KiB in 135 allocs | 46.9 KiB in 197 allocs | 27.4 KiB in 135 allocs | 87.4 KiB in 449 allocs | 44.3 KiB in 1,123 allocs | unsupported | unsupported | unsupported | 0.03× of `yaml-v2` ▲ win | | dump | `gha-workflow` | 9.43 KiB | 9.41 KiB in 1 allocs | 635 KiB in 1,732 allocs | 701 KiB in 2,431 allocs | differs [18] | differs [13] | 648 KiB in 16,318 allocs | unsupported | unsupported | unsupported | 0.01× of `yaml-v3` ▲ win | | dump | `helm-ingress-nginx` | 52.4 KiB | 9.43 KiB in 1 allocs | 1.01e+03 KiB in 2,189 allocs | 1.11 MiB in 3,122 allocs | 667 KiB in 2,188 allocs | 1.01e+03 KiB in 7,136 allocs | 778 KiB in 20,821 allocs | unsupported | unsupported | unsupported | 0.01× of `yaml-v2` ▲ win | | dump | `openapi-api-with-examples` | 6.75 KiB | 4.83 KiB in 1 allocs | 344 KiB in 720 allocs | 379 KiB in 1,024 allocs | 221 KiB in 719 allocs | 392 KiB in 2,321 allocs | 293 KiB in 6,124 allocs | unsupported | unsupported | unsupported | 0.02× of `yaml-v2` ▲ win | | dump | `openapi-petstore` | 5.34 KiB | 6.11 KiB in 1 allocs | 349 KiB in 926 allocs | 391 KiB in 1,320 allocs | 233 KiB in 926 allocs | 443 KiB in 3,289 allocs | 355 KiB in 8,091 allocs | unsupported | unsupported | unsupported | 0.03× of `yaml-v2` ▲ win | | dump | `prometheus-config` | 1.1 KiB | 360 B in 1 allocs | 37.2 KiB in 117 allocs | 43 KiB in 173 allocs | 25.3 KiB in 117 allocs | 83.1 KiB in 390 allocs | 30.7 KiB in 854 allocs | unsupported | unsupported | unsupported | 0.01× of `yaml-v2` ▲ win | | dump | `reference-a` | 51 B | 49 B in 1 allocs | 6.84 KiB in 32 allocs | 7.82 KiB in 47 allocs | 4.77 KiB in 32 allocs | 15.2 KiB in 101 allocs | 5.73 KiB in 175 allocs | unsupported | unsupported | unsupported | 0.01× of `yaml-v2` ▲ win | | dump | `reference-b` | 51 B | 49 B in 1 allocs | 6.84 KiB in 32 allocs | 7.82 KiB in 47 allocs | 4.77 KiB in 32 allocs | 15.2 KiB in 101 allocs | 5.73 KiB in 175 allocs | unsupported | unsupported | unsupported | 0.01× of `yaml-v2` ▲ win | | dump | `records-4k` | 4.14 KiB | 4.84 KiB in 1 allocs | differs [22] | differs [24] | differs [20] | differs [17] | differs [3] | unsupported | unsupported | unsupported | no rival | | dump | `records-64k` | 64.2 KiB | 73.1 KiB in 1 allocs | differs [22] | differs [24] | differs [20] | differs [17] | differs [3] | unsupported | unsupported | unsupported | no rival | | dump | `records-1m` | 1 MiB | 6.01 MiB in 40 allocs | differs [22] | differs [24] | differs [20] | differs [17] | differs [3] | unsupported | unsupported | unsupported | no rival | | dump | `records-8m` | 8 MiB | 47.8 MiB in 49 allocs | differs [22] | differs [24] | differs [20] | differs [17] | differs [3] | unsupported | unsupported | unsupported | no rival | | dump | `compose-merge-2k` | 2.14 KiB | 4.84 KiB in 1 allocs | 350 KiB in 1,167 allocs | 402 KiB in 1,629 allocs | 227 KiB in 1,166 allocs | 429 KiB in 3,638 allocs | 355 KiB in 10,182 allocs | unsupported | unsupported | unsupported | 0.02× of `yaml-v2` ▲ win | | dump | `compose-merge-64k` | 64.1 KiB | 130 KiB in 1 allocs | 18.8 MiB in 33,881 allocs | 20.2 MiB in 47,479 allocs | 12.6 MiB in 33,879 allocs | 15.8 MiB in 106,129 allocs | 10.6 MiB in 307,551 allocs | unsupported | unsupported | unsupported | 0.01× of `goccy-go-yaml` ▲ win | | dump | `compose-merge-1m` | 1 MiB | 12.8 MiB in 55 allocs | 328 MiB in 526,398 allocs | 347 MiB in 737,974 allocs | 221 MiB in 526,385 allocs | 272 MiB in 1,649,647 allocs | 165 MiB in 4,828,718 allocs | unsupported | unsupported | unsupported | 0.08× of `goccy-go-yaml` ▲ win | | dump | `openapi-100k` | 101 KiB | 106 KiB in 1 allocs | 14.5 MiB in 27,536 allocs | 15.7 MiB in 39,491 allocs | 9.79 MiB in 27,535 allocs | 12.8 MiB in 89,944 allocs | 9.23 MiB in 253,914 allocs | unsupported | unsupported | unsupported | 0.01× of `goccy-go-yaml` ▲ win | | dump | `openapi-8m` | 8 MiB | 59.5 MiB in 63 allocs | 1.54 GiB in 2,208,530 allocs | 1.63 GiB in 3,169,591 allocs | 1.04 GiB in 2,208,529 allocs | 1.27 GiB in 7,218,664 allocs | 749 MiB in 20,690,480 allocs | unsupported | unsupported | unsupported | 0.08× of `goccy-go-yaml` ▲ win | | dump | `helm-values-64k` | 64 KiB | 40.7 KiB in 1 allocs | 6.14 MiB in 12,346 allocs | 6.79 MiB in 17,458 allocs | 4.12 MiB in 12,345 allocs | 5.45 MiB in 39,285 allocs | 3.87 MiB in 113,017 allocs | unsupported | unsupported | unsupported | 0.01× of `goccy-go-yaml` ▲ win | | dump | `helm-values-1m` | 1 MiB | 641 KiB in 1 allocs | 130 MiB in 194,889 allocs | 139 MiB in 275,810 allocs | 87 MiB in 194,888 allocs | 108 MiB in 620,604 allocs | 62.1 MiB in 1,815,537 allocs | unsupported | unsupported | unsupported | 0.01× of `goccy-go-yaml` ▲ win | | dump | `deep-block` | 5.23 KiB | 10.8 KiB in 1 allocs | 403 KiB in 648 allocs | 385 KiB in 953 allocs | 250 KiB in 646 allocs | 523 KiB in 1,912 allocs | 890 KiB in 4,293 allocs | unsupported | unsupported | unsupported | 0.04× of `yaml-v2` ▲ win | | dump | `deep-flow` | 202 B | 218 B in 1 allocs | 158 KiB in 29 allocs | 175 KiB in 237 allocs | 98.5 KiB in 29 allocs | 240 KiB in 892 allocs | 51.3 KiB in 1,115 allocs | unsupported | unsupported | unsupported | <0.01× of `goccy-go-yaml` ▲ win | | dump | `wide-map-64k` | 64 KiB | 74.5 KiB in 1 allocs | 8.31 MiB in 20,517 allocs | 9.57 MiB in 32,273 allocs | 5.66 MiB in 20,517 allocs | 8.34 MiB in 68,379 allocs | 6.96 MiB in 261,963 allocs | unsupported | unsupported | unsupported | 0.01× of `yaml-v2` ▲ win | | dump | `wide-map-1m` | 1 MiB | 14.4 MiB in 61 allocs | 164 MiB in 297,118 allocs | 181 MiB in 468,111 allocs | 111 MiB in 297,118 allocs | 150 MiB in 990,610 allocs | 106 MiB in 4,251,146 allocs | unsupported | unsupported | unsupported | 0.14× of `goccy-go-yaml` ▲ win | | dump | `long-scalars-256k` | 261 KiB | 332 KiB in 2 allocs | 1.4 MiB in 68 allocs | 1.27 MiB in 77 allocs | 1.4 MiB in 68 allocs | 4.65 MiB in 50,287 allocs | 5.49 MiB in 3,546 allocs | unsupported | unsupported | unsupported | 0.26× of `yaml-v4` ▲ win | | dump | `flow-heavy-64k` | 64 KiB | 89.5 KiB in 1 allocs | 23.5 MiB in 31,323 allocs | 25.6 MiB in 51,337 allocs | 15.7 MiB in 31,323 allocs | 19.4 MiB in 120,941 allocs | 9.92 MiB in 314,832 allocs | unsupported | unsupported | unsupported | <0.01× of `goccy-go-yaml` ▲ win | | dump | `anchors-32k` | 32 KiB | 155 KiB in 1 allocs | 49.4 MiB in 66,674 allocs | 53 MiB in 100,522 allocs | differs [19] | differs [14] | 19.1 MiB in 600,171 allocs | unsupported | unsupported | unsupported | <0.01× of `goccy-go-yaml` ▲ win | | dump | `unicode-32k` | 32 KiB | 27 KiB in 1 allocs | 1.59 MiB in 2,937 allocs | 1.74 MiB in 4,389 allocs | 1.1 MiB in 2,937 allocs | 1.62 MiB in 12,849 allocs | 1.57 MiB in 39,190 allocs | unsupported | unsupported | unsupported | 0.02× of `yaml-v2` ▲ win | | dump | `scalar-styles-32k` | 32 KiB | 32.5 KiB in 1 allocs | 3.26 MiB in 7,344 allocs | 3.58 MiB in 10,002 allocs | 2.22 MiB in 7,344 allocs | 3.01 MiB in 24,494 allocs | 2.89 MiB in 89,385 allocs | unsupported | unsupported | unsupported | 0.01× of `yaml-v2` ▲ win | | document parse | `compose-wordpress` | 815 B | 7.52 KiB in 21 allocs | differs [21] | differs [23] | unsupported | unsupported | differs [2] | unsupported | unsupported | unsupported | no rival | | document parse | `gha-workflow` | 9.43 KiB | 32.5 KiB in 26 allocs | differs [21] | differs [23] | unsupported | unsupported | differs [2] | unsupported | unsupported | unsupported | no rival | | document parse | `helm-ingress-nginx` | 52.4 KiB | 87.6 KiB in 26 allocs | 485 KiB in 4,453 allocs | differs [23] | unsupported | unsupported | 1.64 MiB in 25,620 allocs | unsupported | unsupported | unsupported | 0.18× of `yaml-v3` ▲ win | | document parse | `k8s-ingress-nginx-multi` | 16 KiB | 62.6 KiB in 30 allocs | differs [21] | 306 KiB in 5,819 allocs | unsupported | unsupported | 1.06 MiB in 20,695 allocs | unsupported | unsupported | unsupported | 0.20× of `yaml-v4` ▲ win | | document parse | `openapi-api-with-examples` | 6.75 KiB | 22.3 KiB in 34 allocs | differs [21] | differs [23] | unsupported | unsupported | differs [2] | unsupported | unsupported | unsupported | no rival | | document parse | `openapi-petstore` | 5.34 KiB | 21 KiB in 26 allocs | 87 KiB in 1,408 allocs | differs [23] | unsupported | unsupported | 351 KiB in 6,263 allocs | unsupported | unsupported | unsupported | 0.24× of `yaml-v3` ▲ win | | document parse | `prometheus-config` | 1.1 KiB | 7.09 KiB in 26 allocs | differs [21] | differs [23] | unsupported | unsupported | differs [2] | unsupported | unsupported | unsupported | no rival | | document parse | `reference-a` | 51 B | 1.93 KiB in 15 allocs | differs [21] | differs [23] | unsupported | unsupported | 7.77 KiB in 192 allocs | unsupported | unsupported | unsupported | 0.25× of `goccy-go-yaml` ▲ win | | document parse | `reference-b` | 51 B | 1.93 KiB in 15 allocs | differs [21] | differs [23] | unsupported | unsupported | 7.77 KiB in 192 allocs | unsupported | unsupported | unsupported | 0.25× of `goccy-go-yaml` ▲ win | | document parse | `records-4k` | 4.14 KiB | 27.4 KiB in 21 allocs | 130 KiB in 2,547 allocs | differs [23] | unsupported | unsupported | differs [5] | unsupported | unsupported | unsupported | 0.21× of `yaml-v3` ▲ win | | document parse | `records-64k` | 64.2 KiB | 325 KiB in 21 allocs | 1.85 MiB in 38,754 allocs | differs [23] | unsupported | unsupported | differs [5] | unsupported | unsupported | unsupported | 0.17× of `yaml-v3` ▲ win | | document parse | `records-1m` | 1 MiB | 4.77 MiB in 21 allocs | 29.1 MiB in 612,225 allocs | differs [23] | unsupported | unsupported | differs [5] | unsupported | unsupported | unsupported | 0.16× of `yaml-v3` ▲ win | | document parse | `records-8m` | 8 MiB | 62.6 MiB in 24 allocs | 230 MiB in 4,840,410 allocs | differs [23] | unsupported | unsupported | differs [5] | unsupported | unsupported | unsupported | 0.27× of `yaml-v3` ▲ win | | document parse | `compose-merge-2k` | 2.14 KiB | 16.2 KiB in 24 allocs | differs [21] | 61.6 KiB in 1,052 allocs | unsupported | unsupported | 219 KiB in 4,339 allocs | unsupported | unsupported | unsupported | 0.26× of `yaml-v4` ▲ win | | document parse | `compose-merge-64k` | 64.1 KiB | 333 KiB in 24 allocs | differs [21] | 1.55 MiB in 28,692 allocs | unsupported | unsupported | 6.99 MiB in 124,038 allocs | unsupported | unsupported | unsupported | 0.21× of `yaml-v4` ▲ win | | document parse | `compose-merge-1m` | 1 MiB | 4.87 MiB in 24 allocs | differs [21] | 24 MiB in 445,014 allocs | unsupported | unsupported | 227 MiB in 1,926,643 allocs | unsupported | unsupported | unsupported | 0.20× of `yaml-v4` ▲ win | | document parse | `k8s-multi-16k` | 16.4 KiB | 90.3 KiB in 33 allocs | differs [21] | differs [23] | unsupported | unsupported | 1.71 MiB in 33,922 allocs | unsupported | unsupported | unsupported | 0.05× of `goccy-go-yaml` ▲ win | | document parse | `k8s-multi-1m` | 1 MiB | 5.11 MiB in 41 allocs | differs [21] | differs [23] | unsupported | unsupported | 169 MiB in 2,080,261 allocs | unsupported | unsupported | unsupported | 0.03× of `goccy-go-yaml` ▲ win | | document parse | `openapi-100k` | 101 KiB | 397 KiB in 29 allocs | 2.02 MiB in 39,624 allocs | differs [23] | unsupported | unsupported | 8.48 MiB in 147,519 allocs | unsupported | unsupported | unsupported | 0.19× of `yaml-v3` ▲ win | | document parse | `openapi-8m` | 8 MiB | 70.2 MiB in 33 allocs | 162 MiB in 3,179,816 allocs | differs [23] | unsupported | unsupported | 1.5 GiB in 11,861,233 allocs | unsupported | unsupported | unsupported | 0.43× of `yaml-v3` ▲ win | | document parse | `helm-values-64k` | 64 KiB | 237 KiB in 24 allocs | differs [21] | differs [23] | unsupported | unsupported | differs [2] | unsupported | unsupported | unsupported | no rival | | document parse | `helm-values-1m` | 1 MiB | 3.4 MiB in 24 allocs | differs [21] | differs [23] | unsupported | unsupported | differs [2] | unsupported | unsupported | unsupported | no rival | | document parse | `deep-block` | 5.23 KiB | 20.6 KiB in 35 allocs | differs [21] | differs [23] | unsupported | unsupported | 170 KiB in 2,453 allocs | unsupported | unsupported | unsupported | 0.12× of `goccy-go-yaml` ▲ win | | document parse | `deep-flow` | 202 B | 37.2 KiB in 51 allocs | 133 KiB in 263 allocs | differs [23] | unsupported | unsupported | 109 KiB in 1,743 allocs | unsupported | unsupported | unsupported | 0.34× of `goccy-go-yaml` ▲ win | | document parse | `wide-map-64k` | 64 KiB | 341 KiB in 17 allocs | 1.87 MiB in 35,393 allocs | 1.87 MiB in 35,396 allocs | unsupported | unsupported | 62.9 MiB in 139,723 allocs | unsupported | unsupported | unsupported | 0.18× of `yaml-v3` ▲ win | | document parse | `wide-map-1m` | 1 MiB | 4.85 MiB in 17 allocs | 28.3 MiB in 513,131 allocs | 28.3 MiB in 513,133 allocs | unsupported | unsupported | 11.2 GiB in 2,025,750 allocs | unsupported | unsupported | unsupported | 0.17× of `yaml-v4` ▲ win | | document parse | `long-scalars-256k` | 261 KiB | 267 KiB in 16 allocs | differs [21] | differs [23] | unsupported | unsupported | 9.92 MiB in 127,403 allocs | unsupported | unsupported | unsupported | 0.03× of `goccy-go-yaml` ▲ win | | document parse | `flow-heavy-64k` | 64 KiB | 477 KiB in 24 allocs | 2.87 MiB in 53,733 allocs | 2.92 MiB in 56,997 allocs | unsupported | unsupported | 14.1 MiB in 255,155 allocs | unsupported | unsupported | unsupported | 0.16× of `yaml-v3` ▲ win | | document parse | `anchors-32k` | 32 KiB | 215 KiB in 25 allocs | 1.02e+03 KiB in 17,019 allocs | differs [23] | unsupported | unsupported | 6.24 MiB in 122,349 allocs | unsupported | unsupported | unsupported | 0.21× of `yaml-v3` ▲ win | | document parse | `unicode-32k` | 32 KiB | 92.5 KiB in 17 allocs | differs [21] | differs [23] | unsupported | unsupported | differs [4] | unsupported | unsupported | unsupported | no rival | | document parse | `scalar-styles-32k` | 32 KiB | 125 KiB in 19 allocs | differs [21] | differs [23] | unsupported | unsupported | 3.24 MiB in 63,472 allocs | unsupported | unsupported | unsupported | 0.04× of `goccy-go-yaml` ▲ win | | unchanged emit | `compose-wordpress` | 815 B | 0 B in 0 allocs | differs [21] | differs [23] | unsupported | unsupported | differs [2] | unsupported | unsupported | unsupported | no rival | | unchanged emit | `gha-workflow` | 9.43 KiB | 0 B in 0 allocs | differs [21] | differs [23] | unsupported | unsupported | differs [2] | unsupported | unsupported | unsupported | no rival | | unchanged emit | `helm-ingress-nginx` | 52.4 KiB | 0 B in 0 allocs | 1.09 MiB in 1,455 allocs | differs [23] | unsupported | unsupported | 744 KiB in 5,484 allocs | unsupported | unsupported | unsupported | <0.01× of `goccy-go-yaml` ▲ win | | unchanged emit | `k8s-ingress-nginx-multi` | 16 KiB | 0 B in 0 allocs | differs [21] | 1.47 MiB in 2,174 allocs | unsupported | unsupported | 291 KiB in 4,198 allocs | unsupported | unsupported | unsupported | <0.01× of `goccy-go-yaml` ▲ win | | unchanged emit | `openapi-api-with-examples` | 6.75 KiB | 0 B in 0 allocs | differs [21] | differs [23] | unsupported | unsupported | differs [2] | unsupported | unsupported | unsupported | no rival | | unchanged emit | `openapi-petstore` | 5.34 KiB | 0 B in 0 allocs | 331 KiB in 470 allocs | differs [23] | unsupported | unsupported | 84.3 KiB in 843 allocs | unsupported | unsupported | unsupported | <0.01× of `goccy-go-yaml` ▲ win | | unchanged emit | `prometheus-config` | 1.1 KiB | 0 B in 0 allocs | differs [21] | differs [23] | unsupported | unsupported | differs [2] | unsupported | unsupported | unsupported | no rival | | unchanged emit | `reference-a` | 51 B | 0 B in 0 allocs | differs [21] | differs [23] | unsupported | unsupported | 641 B in 28 allocs | unsupported | unsupported | unsupported | <0.01× of `goccy-go-yaml` ▲ win | | unchanged emit | `reference-b` | 51 B | 0 B in 0 allocs | differs [21] | differs [23] | unsupported | unsupported | 641 B in 28 allocs | unsupported | unsupported | unsupported | <0.01× of `goccy-go-yaml` ▲ win | | unchanged emit | `records-4k` | 4.14 KiB | 0 B in 0 allocs | 587 KiB in 1,079 allocs | differs [23] | unsupported | unsupported | differs [5] | unsupported | unsupported | unsupported | <0.01× of `yaml-v3` ▲ win | | unchanged emit | `records-64k` | 64.2 KiB | 0 B in 0 allocs | 10.7 MiB in 16,254 allocs | differs [23] | unsupported | unsupported | differs [5] | unsupported | unsupported | unsupported | <0.01× of `yaml-v3` ▲ win | | unchanged emit | `records-1m` | 1 MiB | 0 B in 0 allocs | 201 MiB in 258,474 allocs | differs [23] | unsupported | unsupported | differs [5] | unsupported | unsupported | unsupported | <0.01× of `yaml-v3` ▲ win | | unchanged emit | `records-8m` | 8 MiB | 0 B in 0 allocs | 1.5 GiB in 2,044,451 allocs | differs [23] | unsupported | unsupported | differs [5] | unsupported | unsupported | unsupported | <0.01× of `yaml-v3` ▲ win | | unchanged emit | `compose-merge-2k` | 2.14 KiB | 0 B in 0 allocs | differs [21] | 168 KiB in 421 allocs | unsupported | unsupported | 28.5 KiB in 615 allocs | unsupported | unsupported | unsupported | <0.01× of `goccy-go-yaml` ▲ win | | unchanged emit | `compose-merge-64k` | 64.1 KiB | 0 B in 0 allocs | differs [21] | 10.6 MiB in 11,220 allocs | unsupported | unsupported | 880 KiB in 17,471 allocs | unsupported | unsupported | unsupported | <0.01× of `goccy-go-yaml` ▲ win | | unchanged emit | `compose-merge-1m` | 1 MiB | 0 B in 0 allocs | differs [21] | 200 MiB in 173,701 allocs | unsupported | unsupported | 14.3 MiB in 271,334 allocs | unsupported | unsupported | unsupported | <0.01× of `goccy-go-yaml` ▲ win | | unchanged emit | `k8s-multi-16k` | 16.4 KiB | 0 B in 0 allocs | differs [21] | differs [23] | unsupported | unsupported | 263 KiB in 5,217 allocs | unsupported | unsupported | unsupported | <0.01× of `goccy-go-yaml` ▲ win | | unchanged emit | `k8s-multi-1m` | 1 MiB | 0 B in 0 allocs | differs [21] | differs [23] | unsupported | unsupported | 15.8 MiB in 319,197 allocs | unsupported | unsupported | unsupported | <0.01× of `goccy-go-yaml` ▲ win | | unchanged emit | `openapi-100k` | 101 KiB | 0 B in 0 allocs | 13.8 MiB in 13,789 allocs | differs [23] | unsupported | unsupported | 1.78 MiB in 24,314 allocs | unsupported | unsupported | unsupported | <0.01× of `goccy-go-yaml` ▲ win | | unchanged emit | `openapi-8m` | 8 MiB | 0 B in 0 allocs | 1.49 GiB in 1,104,293 allocs | differs [23] | unsupported | unsupported | 143 MiB in 1,952,872 allocs | unsupported | unsupported | unsupported | <0.01× of `goccy-go-yaml` ▲ win | | unchanged emit | `helm-values-64k` | 64 KiB | 0 B in 0 allocs | differs [21] | differs [23] | unsupported | unsupported | differs [2] | unsupported | unsupported | unsupported | no rival | | unchanged emit | `helm-values-1m` | 1 MiB | 0 B in 0 allocs | differs [21] | differs [23] | unsupported | unsupported | differs [2] | unsupported | unsupported | unsupported | no rival | | unchanged emit | `deep-block` | 5.23 KiB | 0 B in 0 allocs | differs [21] | differs [23] | unsupported | unsupported | 387 KiB in 406 allocs | unsupported | unsupported | unsupported | <0.01× of `goccy-go-yaml` ▲ win | | unchanged emit | `deep-flow` | 202 B | 0 B in 0 allocs | 159 KiB in 30 allocs | differs [23] | unsupported | unsupported | 12.7 KiB in 203 allocs | unsupported | unsupported | unsupported | <0.01× of `goccy-go-yaml` ▲ win | | unchanged emit | `wide-map-64k` | 64 KiB | 0 B in 0 allocs | 8.16 MiB in 15,522 allocs | 8.16 MiB in 16,142 allocs | unsupported | unsupported | 645 KiB in 11,813 allocs | unsupported | unsupported | unsupported | <0.01× of `goccy-go-yaml` ▲ win | | unchanged emit | `wide-map-1m` | 1 MiB | 0 B in 0 allocs | 161 MiB in 225,067 allocs | 161 MiB in 234,068 allocs | unsupported | unsupported | 10.1 MiB in 171,066 allocs | unsupported | unsupported | unsupported | <0.01× of `goccy-go-yaml` ▲ win | | unchanged emit | `long-scalars-256k` | 261 KiB | 0 B in 0 allocs | differs [21] | differs [23] | unsupported | unsupported | 1.7 MiB in 41 allocs | unsupported | unsupported | unsupported | <0.01× of `goccy-go-yaml` ▲ win | | unchanged emit | `flow-heavy-64k` | 64 KiB | 0 B in 0 allocs | 23.1 MiB in 20,056 allocs | 23.1 MiB in 20,060 allocs | unsupported | unsupported | 1.05 MiB in 31,539 allocs | unsupported | unsupported | unsupported | <0.01× of `goccy-go-yaml` ▲ win | | unchanged emit | `anchors-32k` | 32 KiB | 0 B in 0 allocs | 5.89 MiB in 6,161 allocs | differs [23] | unsupported | unsupported | 628 KiB in 21,041 allocs | unsupported | unsupported | unsupported | <0.01× of `goccy-go-yaml` ▲ win | | unchanged emit | `unicode-32k` | 32 KiB | 0 B in 0 allocs | differs [21] | differs [23] | unsupported | unsupported | differs [4] | unsupported | unsupported | unsupported | no rival | | unchanged emit | `scalar-styles-32k` | 32 KiB | 0 B in 0 allocs | differs [21] | differs [23] | unsupported | unsupported | 584 KiB in 13,294 allocs | unsupported | unsupported | unsupported | <0.01× of `goccy-go-yaml` ▲ win | | edit script | `compose-wordpress` | 815 B | 9.26 KiB in 92 allocs | differs [21] | differs [23] | unsupported | unsupported | differs [2] | unsupported | unsupported | unsupported | no rival | | edit script | `gha-workflow` | 9.43 KiB | 8.88 KiB in 89 allocs | differs [21] | differs [23] | unsupported | unsupported | differs [2] | unsupported | unsupported | unsupported | no rival | | edit script | `helm-ingress-nginx` | 52.4 KiB | 1.94 KiB in 40 allocs | 1.12 MiB in 1,564 allocs | differs [23] | unsupported | unsupported | 729 KiB in 5,698 allocs | unsupported | unsupported | unsupported | <0.01× of `goccy-go-yaml` ▲ win | | edit script | `openapi-api-with-examples` | 6.75 KiB | 1.2 KiB in 22 allocs | differs [21] | differs [23] | unsupported | unsupported | differs [2] | unsupported | unsupported | unsupported | no rival | | edit script | `openapi-petstore` | 5.34 KiB | 11.6 KiB in 104 allocs | 321 KiB in 455 allocs | differs [23] | unsupported | unsupported | differs [2] | unsupported | unsupported | unsupported | 0.04× of `yaml-v3` ▲ win | | edit script | `prometheus-config` | 1.1 KiB | 1.16 KiB in 20 allocs | differs [21] | differs [23] | unsupported | unsupported | differs [2] | unsupported | unsupported | unsupported | no rival | | edit script | `reference-a` | 51 B | 2.35 KiB in 27 allocs | differs [21] | differs [23] | unsupported | unsupported | 4.64 KiB in 146 allocs | unsupported | unsupported | unsupported | 0.51× of `goccy-go-yaml` ▲ win | | edit script | `reference-b` | 51 B | 2.36 KiB in 28 allocs | differs [21] | differs [23] | unsupported | unsupported | differs [1] | unsupported | unsupported | unsupported | no rival | | edit script | `records-4k` | 4.14 KiB | 4.35 KiB in 69 allocs | 609 KiB in 1,140 allocs | differs [23] | unsupported | unsupported | differs [5] | unsupported | unsupported | unsupported | <0.01× of `yaml-v3` ▲ win | | edit script | `records-64k` | 64.2 KiB | 4.35 KiB in 69 allocs | 10.7 MiB in 16,311 allocs | differs [23] | unsupported | unsupported | differs [5] | unsupported | unsupported | unsupported | <0.01× of `yaml-v3` ▲ win | | edit script | `records-1m` | 1 MiB | 4.35 KiB in 69 allocs | 201 MiB in 258,530 allocs | differs [23] | unsupported | unsupported | differs [5] | unsupported | unsupported | unsupported | <0.01× of `yaml-v3` ▲ win | | edit script | `records-8m` | 8 MiB | 4.35 KiB in 69 allocs | 1.5 GiB in 2,044,508 allocs | differs [23] | unsupported | unsupported | differs [5] | unsupported | unsupported | unsupported | <0.01× of `yaml-v3` ▲ win | | edit script | `compose-merge-2k` | 2.14 KiB | 5.44 KiB in 63 allocs | differs [21] | 179 KiB in 457 allocs | unsupported | unsupported | 32.8 KiB in 729 allocs | unsupported | unsupported | unsupported | 0.17× of `goccy-go-yaml` ▲ win | | edit script | `compose-merge-64k` | 64.1 KiB | 13.8 KiB in 65 allocs | differs [21] | 10.6 MiB in 11,256 allocs | unsupported | unsupported | 876 KiB in 17,584 allocs | unsupported | unsupported | unsupported | 0.02× of `goccy-go-yaml` ▲ win | | edit script | `compose-merge-1m` | 1 MiB | 134 KiB in 65 allocs | differs [21] | 200 MiB in 173,737 allocs | unsupported | unsupported | 14.3 MiB in 271,445 allocs | unsupported | unsupported | unsupported | <0.01× of `goccy-go-yaml` ▲ win | | edit script | `openapi-100k` | 101 KiB | 10.1 KiB in 83 allocs | 13.8 MiB in 13,781 allocs | differs [23] | unsupported | unsupported | 1.78 MiB in 24,434 allocs | unsupported | unsupported | unsupported | <0.01× of `goccy-go-yaml` ▲ win | | edit script | `openapi-8m` | 8 MiB | 10.4 KiB in 84 allocs | 1.49 GiB in 1,104,287 allocs | differs [23] | unsupported | unsupported | 143 MiB in 1,953,003 allocs | unsupported | unsupported | unsupported | <0.01× of `goccy-go-yaml` ▲ win | | edit script | `helm-values-64k` | 64 KiB | 1.61 KiB in 32 allocs | differs [21] | differs [23] | unsupported | unsupported | differs [2] | unsupported | unsupported | unsupported | no rival | | edit script | `helm-values-1m` | 1 MiB | 65.9 KiB in 34 allocs | differs [21] | differs [23] | unsupported | unsupported | differs [2] | unsupported | unsupported | unsupported | no rival | | edit script | `wide-map-64k` | 64 KiB | 4.7 KiB in 47 allocs | 8.03 MiB in 15,549 allocs | 8.04 MiB in 16,174 allocs | unsupported | unsupported | 622 KiB in 11,862 allocs | unsupported | unsupported | unsupported | <0.01× of `goccy-go-yaml` ▲ win | | edit script | `wide-map-1m` | 1 MiB | 4.7 KiB in 47 allocs | 159 MiB in 225,094 allocs | 159 MiB in 234,099 allocs | unsupported | unsupported | 10.1 MiB in 171,114 allocs | unsupported | unsupported | unsupported | <0.01× of `goccy-go-yaml` ▲ win | | patch (RFC 6902) | `compose-wordpress` | 815 B | 17.6 KiB in 121 allocs † | unsupported | unsupported | unsupported | unsupported | unsupported | differs [9] | differs [11] | 116 KiB in 1,048 allocs † | 0.15× of `json-patch` † (text API, value-only) ▲ win | | patch (RFC 6902) | `gha-workflow` | 9.43 KiB | 54.3 KiB in 130 allocs † | unsupported | unsupported | unsupported | unsupported | unsupported | differs [9] | differs [11] | rejects [6] | no rival | | patch (RFC 6902) | `reference-a` | 51 B | 4.33 KiB in 42 allocs † | unsupported | unsupported | unsupported | unsupported | unsupported | differs [9] | 10.1 KiB in 276 allocs † | 23 KiB in 204 allocs † | 0.43× of `palantir-yamlpatch-goccy` † (text API) ▲ win | | patch (RFC 6902) | `reference-b` | 51 B | 4.34 KiB in 43 allocs † | unsupported | unsupported | unsupported | unsupported | unsupported | differs [8] | differs [10] | 22.9 KiB in 204 allocs † | 0.19× of `json-patch` † (text API, value-only) ▲ win | | patch (RFC 6902) | `records-4k` | 4.14 KiB | 37.6 KiB in 91 allocs † | unsupported | unsupported | unsupported | unsupported | unsupported | 775 KiB in 3,830 allocs † | differs [12] | differs [7] | 0.05× of `palantir-yamlpatch` † (text API) ▲ win | | patch (RFC 6902) | `records-64k` | 64.2 KiB | 402 KiB in 91 allocs † | unsupported | unsupported | unsupported | unsupported | unsupported | 12.6 MiB in 55,213 allocs † | differs [12] | differs [7] | 0.03× of `palantir-yamlpatch` † (text API) ▲ win | | patch (RFC 6902) | `records-1m` | 1 MiB | 5.85 MiB in 91 allocs † | unsupported | unsupported | unsupported | unsupported | unsupported | 230 MiB in 870,901 allocs † | differs [12] | differs [7] | 0.03× of `palantir-yamlpatch` † (text API) ▲ win | | patch (RFC 6902) | `records-8m` | 8 MiB | 71.1 MiB in 94 allocs † | unsupported | unsupported | unsupported | unsupported | unsupported | 1.73 GiB in 6,885,062 allocs † | differs [12] | differs [7] | 0.04× of `palantir-yamlpatch` † (text API) ▲ win | Cells without a number, with what the correctness guard found: 1. `goccy-go-yaml` (differs; edit-script ×1): Does not keep the block literal style of the edited value: the new text is written as a plain scalar. What the guard found, in edit-script on `reference-b`: `byte 26: got 48 bytes "#abc 123\nb: this is a test2\nc: 13\n", expected 53 bytes "#abc 123\nb: |-\n this is a test2\nc: "; line 5: got "b: this is a test2", expected "b: |-"` 2. `goccy-go-yaml` (differs; document-parse ×6, unchanged-emit ×6, edit-script ×7): The AST prints its own spacing for flow collections and trailing comments, and blank lines, quoting or a leading --- marker change, so the text it writes back is not the input. What the guard found, in document-parse on `compose-wordpress`: `does not write the input back: byte 807: got 807 bytes "\n db_data:\n", expected 815 bytes "\n db_data:\n\n \n "; the texts differ only in blank lines` 3. `goccy-go-yaml` (differs; dump ×4): 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 `records-4k`: `read back: at /14/address/zip: got 5509, expected "05509"` 4. `goccy-go-yaml` (differs; document-parse ×1, unchanged-emit ×1): Writes the escapes of a double-quoted scalar (\u00e9) as the characters themselves. What the guard found, in document-parse on `unicode-32k`: `does not write the input back: byte 20: got 28160 bytes " café 0\n- \"é中文 0\"\n- \"😀 grin", expected 32800 bytes " café 0\n- \"\\u00e9\\u4e2d\\u6587 0\"\n- "; line 2: got "- \"é中文 0\"", expected "- \…` 5. `goccy-go-yaml` (differs; document-parse ×4, unchanged-emit ×4, edit-script ×4): Writes the null spelled ~ as null. What the guard found, in document-parse on `records-4k`: `does not write the input back: byte 736: got 4275 bytes ".25\n note: null\n- id: 3\n name: Eps", expected 4242 bytes ".25\n note: ~\n- id: 3\n name: Epsilo"; line 47: got " note: null", expected…` 6. `json-patch` (rejects; patch ×1): Reads the document through sigs.k8s.io/yaml, a YAML 1.1 reader: the key on is the boolean true, so a path under /on is not found. What the guard found, in patch on `gha-workflow`: `add operation does not apply: doc is missing path: "/on/push/branches/-": missin…` 7. `json-patch` (differs; patch ×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 patch on `records-4k`: `value after the patch: at /1/score: got 812, expected 812 (float)` 8. `palantir-yamlpatch` (differs; patch ×1): Does not keep the block literal style of the edited value: the new text is written as a plain scalar. What the guard found, in patch on `reference-b`: `byte 13: got 47 bytes "hello\na: 10\n#abc 123\nb: this is a te", expected 53 bytes "hello\na: 10\n\n#abc 123\nb: |-\n this i"; line 3: got "#abc 123", expected ""` 9. `palantir-yamlpatch` (differs; patch ×3): Patches through the yaml.v3 node tree, which keeps comments but not blank lines, spacing or a leading --- marker, so the text it writes back differs from the input. What the guard found, in patch on `compose-wordpress`: `byte 852: got 852 bytes "\n db_data:\n", expected 860 bytes "\n db_data:\n\n \n "; the texts differ only in blank lines` 10. `palantir-yamlpatch-goccy` (differs; patch ×1): Does not keep the block literal style of the edited value: the new text is written as a plain scalar. What the guard found, in patch on `reference-b`: `byte 26: got 48 bytes "#abc 123\nb: this is a test2\nc: 13\n", expected 53 bytes "#abc 123\nb: |-\n this is a test2\nc: "; line 5: got "b: this is a test2", expected "b: |-"` 11. `palantir-yamlpatch-goccy` (differs; patch ×2): Patches through the goccy AST, which prints its own spacing and changes blank lines, so the text it writes back differs from the input. What the guard found, in patch on `compose-wordpress`: `byte 852: got 852 bytes "\n db_data:\n", expected 860 bytes "\n db_data:\n\n \n "; the texts differ only in blank lines` 12. `palantir-yamlpatch-goccy` (differs; patch ×4): Writes the null spelled ~ as null. What the guard found, in patch on `records-4k`: `byte 752: got 4244 bytes ".25\n note: null\n- id: 3\n name: Eps", expected 4211 bytes ".25\n note: ~\n- id: 3\n name: Epsilo"; line 48: got " note: null", expected " note: ~"` 13. `sigs-yaml` (differs; typed-load ×1, generic-load ×1, dump ×1): A YAML 1.1 reader: the key on is the boolean true, so /on is not found. What the guard found, in typed-load on `gha-workflow`: `/on/push/branches: 0 items, expected 2` 14. `sigs-yaml` (differs; dump ×1): A YAML 1.1 reader: the plain scalar y is the boolean true. What the guard found, in dump on `anchors-32k`: `read back: at /base/b/1: got true, expected "y"` 15. `sigs-yaml` (differs; generic-load ×3): Reads one document of a multi-document stream, so the stream does not come back whole. What the guard found, in generic-load on `k8s-ingress-nginx-multi`: `at /: got a mapping of 3, expected a sequence of 19` 16. `sigs-yaml` (differs; generic-load ×19): Reads through JSON, where every number is a float, so an integer comes back as a float. What the guard found, in generic-load on `compose-wordpress`: `at /services/db/expose/0: got 3306 (float), expected 3306` 17. `sigs-yaml` (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 (float)` 18. `yaml-v2` (differs; generic-load ×1, dump ×1): A YAML 1.1 reader: the key on is the boolean true, so /on is not found. What the guard found, in generic-load on `gha-workflow`: `missing /on` 19. `yaml-v2` (differs; generic-load ×1, dump ×1): A YAML 1.1 reader: the plain scalar y is the boolean true. What the guard found, in generic-load on `anchors-32k`: `at /base/b/1: got true, expected "y"` 20. `yaml-v2` (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 (float)` 21. `yaml-v3` (differs; document-parse ×18, unchanged-emit ×18, edit-script ×11): The yaml.Node tree keeps comments but not blank lines, spacing or a leading --- marker, and it lays flow collections out its own way, so the text it writes back is not the input. What the guard found, in document-parse on `compose-wordpress`: `does not write the input back: byte 807: got 807 bytes "\n db_data:\n", expected 815 bytes "\n db_data:\n\n \n "; the texts differ only in blank lines` 22. `yaml-v3` (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 (float)` 23. `yaml-v4` (differs; document-parse ×24, unchanged-emit ×24, edit-script ×16): The yaml.Node tree keeps comments but not blank lines, spacing or a leading --- marker, and it lays flow collections out its own way, so the text it writes back is not the input. What the guard found, in document-parse on `compose-wordpress`: `does not write the input back: byte 303: got 783 bytes "olumes:\n - db_data:/var/lib/mysql", expected 815 bytes "olumes:\n - db_data:/var/lib/mys"; the texts differ only in blank lines and in…` 24. `yaml-v4` (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 (float)` 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. Method: **full run** of 2026-10-16T21:48:45Z. Kit revision 0abba1d8b4f2. `oneyaml-go eea67bb6b962`; competitors `yaml-v3 v3.0.5`, `yaml-v4 v4.0.0-rc.6`, `yaml-v2 v2.4.0`, `sigs-yaml v1.6.0`, `goccy-go-yaml v1.19.2`, `palantir-yamlpatch v1.5.0`, `palantir-yamlpatch-goccy v1.5.0`, `json-patch v5.9.11`. Runtime go1.26.0 (target linux/amd64), measured with testing.B: ns/op from testing.Benchmark at benchtime 1s. Memory: B/op and allocs/op from testing.B (ReportAllocs). 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 2.67 before, 4.06 after, at most 11.23 during). # Conformance What oneyaml-go 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: `Parse`, `Unmarshal`, `Marshal`, `ApplyPatch` and `ApplyMergePatch`. 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-go` with `KIT_ONEYAML_GO_SRC` set to a checkout of this repository. The tests in this repository that run 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 kit case fails CI. Locally they are skipped unless `ONEYAML_KIT` names a kit checkout. ## Results The region below is the kit's own report for this library, copied as generated. Method: kit 0.1.0 (corpus digest `b1c5e8ce5ca5`, kit revision `0abba1d8b4f2`), every case including the slow ones, run 2026-10-16, library revision `eea67bb6b962`, yaml-test-suite `data-2022-01-17` (revision `6e6c296a`) plus 605 kit cases. Generated by `bin/kit run --slow --adapter oneyaml-go` from the kit's result file `oneyaml-go.json`; do not edit. ## oneyaml-go eea67bb6b962 Language: go. Run date: 2026-10-16. 1286 cases in 11.252 s (full mode). | Group | Cases | Loose | Strict | Strict, corrected | Unsupported | Strict only | Emulated | |---|---:|---:|---:|---:|---:|---:|---:| | yaml-test-suite · events · invalid input | 94 | 94/94 | 0/94 | 0/94 | 0 | 94 | 0 | | yaml-test-suite · events · valid input | 308 | 308/308 | 308/308 | – | 0 | 0 | 0 | | yaml-test-suite · values (in.json) · valid input | 279 | 278/279 | 278/279 | 279/279 | 0 | 0 | 0 | | kit cases · diagnostics · events | 45 | 45/45 | 45/45 | – | 0 | 0 | 0 | | kit cases · diagnostics · values | 2 | 2/2 | 2/2 | – | 0 | 0 | 0 | | kit cases · edit · edits by patch | 324 | 324/324 | 324/324 | – | 0 | 0 | 0 | | kit cases · emit · values written | 33 | 33/33 | 33/33 | – | 0 | 0 | 0 | | kit cases · parse · events | 34 | 34/34 | 34/34 | – | 0 | 0 | 0 | | kit cases · parse · values | 19 | 19/19 | 19/19 | – | 0 | 0 | 0 | | kit cases · roundtrip · edits by patch | 30 | 30/30 | 30/30 | – | 0 | 0 | 0 | | kit cases · safety · edits by patch | 11 | 11/11 | 11/11 | – | 0 | 0 | 0 | | kit cases · safety · events | 32 | 32/32 | 32/32 | – | 0 | 0 | 0 | | kit cases · safety · values | 17 | 17/17 | 17/17 | – | 0 | 0 | 0 | | kit cases · schema · edits by patch | 3 | 3/3 | 3/3 | – | 0 | 0 | 0 | | kit cases · schema · values | 55 | 49/55 | 49/55 | – | 0 | 0 | 0 | | **yaml-test-suite · events · all 402** | 402 | 402/402 | 308/402 | 308/402 | 0 | 94 | 0 | Cases whose expected bytes rest on a kit convention (the specification does not decide them; see [the kit's edit conventions](https://github.com/recursive-ai-oss/oneyaml-kit/blob/main/docs/edit-semantics.md)). They are counted in the table above too. | Group | Cases | Loose pass | Loose fail | Strict pass | Strict fail | Unsupported | |---|---:|---:|---:|---:|---:|---:| | kit cases · edit · edits by patch | 90 | 90 | 0 | 90 | 0 | 0 | | kit cases · parse · values | 1 | 1 | 0 | 1 | 0 | 0 | | kit cases · safety · edits by patch | 4 | 4 | 0 | 4 | 0 | 0 | | kit cases · schema · edits by patch | 3 | 3 | 0 | 3 | 0 | 0 | | kit cases · schema · values | 8 | 8 | 0 | 8 | 0 | 0 | | **all** | 106 | 106 | 0 | 106 | 0 | 0 | Cases where the strict verdict adds the family's writing convention to the specification (loose is the specification). They are counted in the table above too. | Group | Cases | Loose pass | Strict pass | Fail only strict | Unsupported | |---|---:|---:|---:|---:|---:| | kit cases · emit · values written | 12 | 12 | 12 | 0 | 0 | Known deviations listed: 14. Unlisted failures: 0. Stale entries: 0. | Case | Expectation | Mode | Verdict | Reason | |---|---|---|---|---| | `own:schema/code-constructing-tags-are-kept-or-refused` | primary | loose | fail | accepted with a wrong result: $[0]: expected {'$tagged': {'tag': 'tag:yaml.org,2002:python/object/appl..., got ['echo hi'] | | `own:schema/code-constructing-tags-are-kept-or-refused` | primary | strict | fail | accepted with a wrong result: $[0]: expected {'$tagged': {'tag': 'tag:yaml.org,2002:python/object/appl..., got ['echo hi'] | | `own:schema/global-tag-uri-is-kept` | primary | loose | fail | Value differs at $[0]: expected {'$tagged': {'tag': 'tag:example.com,2000:shape', 'value'..., got [1, 2] | | `own:schema/global-tag-uri-is-kept` | primary | strict | fail | Value differs at $[0]: expected {'$tagged': {'tag': 'tag:example.com,2000:shape', 'value'..., got [1, 2] | | `own:schema/local-tag-on-a-collection-is-kept` | primary | loose | fail | Value differs at $[0]: expected {'$tagged': {'tag': '!shape', 'value': {'kind': 'circle',..., got {'kind': 'circle', 'r': 2} | | `own:schema/local-tag-on-a-collection-is-kept` | primary | strict | fail | Value differs at $[0]: expected {'$tagged': {'tag': '!shape', 'value': {'kind': 'circle',..., got {'kind': 'circle', 'r': 2} | | `own:schema/mapping-as-a-mapping-key` | primary | loose | fail | Rejects valid input: 'oneyaml: line 1, column 3: a collection cannot be the key of a generic mapping (Go map keys must be comparable)' | | `own:schema/mapping-as-a-mapping-key` | primary | strict | fail | Rejects valid input: 'oneyaml: line 1, column 3: a collection cannot be the key of a generic mapping (Go map keys must be comparable)' | | `own:schema/sequence-as-a-mapping-key` | primary | loose | fail | Rejects valid input: 'oneyaml: line 1, column 3: a collection cannot be the key of a generic mapping (Go map keys must be comparable)' | | `own:schema/sequence-as-a-mapping-key` | primary | strict | fail | Rejects valid input: 'oneyaml: line 1, column 3: a collection cannot be the key of a generic mapping (Go map keys must be comparable)' | | `own:schema/unresolved-tag-is-kept` | primary | loose | fail | Value differs at $[0]: expected {'$tagged': {'tag': '!color', 'value': 'red'}}, got 'red' | | `own:schema/unresolved-tag-is-kept` | primary | strict | fail | Value differs at $[0]: expected {'$tagged': {'tag': '!color', 'value': 'red'}}, got 'red' | | `yts:UGM3@value` | primary | loose | fail | Value differs at $[0].product[0].price: expected 450, got 450.0 | | `yts:UGM3@value` | primary | strict | fail | Value differs at $[0].product[0].price: expected 450, got 450.0 | ### How to read the table - **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. - **Strict, corrected** judges the cases where the upstream expectation is wrong against the kit's corrected one. The kit lists the corrections and their reasons. - **Unsupported** and **Strict only** count cases the adapter cannot judge. They are not passes. ### Invalid input and strict events For invalid input, the Loose column counts the inputs the library rejects. The Strict column also compares the events emitted before the error, and `Parse` returns no partial document, so the kit's adapter has no events to hand over for a failing input and the strict check cannot count a pass for that row. This is a gap in what the public API exposes, not a case the library accepts. The error category and position are checked by the kit's diagnostics cases, which have rows of their own. ## Known deviations The cases below are the ones the library does not pass. Each one is a difference in the Go value `Unmarshal` into `any` produces, not in what the parser accepts; the `Document` and `Node` API keep the information. | Case | What happens | Where to get the full information | |---|---|---| | `yts:UGM3@value` | The suite's `in.json` records `450` for the scalar `450.00`; the Core schema reads it as the float `450.0`. The kit's corrected expectation agrees with the library (see Strict, corrected). | none needed | | `own:schema/unresolved-tag-is-kept` | A local tag the schema does not know, such as `!color red`, reads as the string `red`. The tag is dropped from the generic value. | `Node.ExplicitTag`, `Node.TagAsWritten` | | `own:schema/local-tag-on-a-collection-is-kept` | A local tag on a mapping or sequence is dropped from the generic value. | `Node.ExplicitTag` | | `own:schema/global-tag-uri-is-kept` | A global tag URI such as `tag:example.com,2000:shape` is dropped from the generic value. | `Node.ExplicitTag` | | `own:schema/code-constructing-tags-are-kept-or-refused` | A tag such as `!!python/object/apply:os.system` is dropped and the value read as plain data. Nothing is constructed or run. | `Node.ExplicitTag` | | `own:schema/mapping-as-a-mapping-key` | `Unmarshal` into `any` refuses a mapping used as a key, because a Go map cannot hold it. | `Parse`, `Node.Entry` | | `own:schema/sequence-as-a-mapping-key` | The same for a sequence used as a key. | `Parse`, `Node.Entry` | Each of these appears twice in the table of deviations in the report above, once for the loose check and once for the strict one. The kit fails the run if a case fails that is not on this list, and fails it if an entry on the list starts to pass, so the list cannot go stale unnoticed. ## 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. - 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 what correct behaviour is where the specification leaves a choice, such as where a new mapping key goes. # 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/). Breaking changes are listed first, under Breaking, each with what to change in your code (releases up to 0.3.0 mark them **Breaking** instead). A new major version has a new module path (`oneyaml.dev/go/v2`), and the entry says so. What a release promises, and the supported Go versions, are under [Stability](https://recursiveai.org/oneyaml/go/docs/overview/#stability). ## [Unreleased] ## [1.0.0] - 2026-10-16 The first stable release. From 1.0.0 the API is stable under the promise in [Stability](https://recursiveai.org/oneyaml/go/docs/overview/#stability). ### Breaking - `LimitError.Limit` is now of the new type `Limit` and names the `LimitSet` field of the bound (`LimitMaxDepth`, `LimitMaxAliasNodes` and so on). It used to be free text such as `"depth"`, `"alias expansion nodes"` or `"patch output size"`. The text is now in the new field `What`. Patch limits report the field that governs them: `MaxInputBytes` for the patch text and for the document a patch makes, and `MaxAliasBytes` for the text a patch copies. **Migrate:** compare `le.Limit` with the `Limit` constants, or match a sentinel such as `errors.Is(err, oneyaml.ErrMaxDepth)`, and read `le.What` where you printed `le.Limit`. - `LimitError.Option` now has one form, `Limits (MaxDepth)`, which names the option and the `LimitSet` field that raise the bound. It used to be `Limits`, `Limits or NoLimits`, `Limits (MaxAliasNodes)` or `none: split the stream`, depending on where the limit was hit. It is empty for the three bounds no option changes: the nesting of a patch value (10,000 levels), the nesting of typed Go values (100,000 levels) and the 2 GiB size of a `Document`. It is also always empty in `compat/yamlv3` and `compat/k8syaml`, which take no limit options and sometimes suggested one. **Migrate:** switch on `le.Limit` rather than reading `Option`. - `SyntaxError.Category` is now of the new type `Category`, and `CategorySyntax`, `CategoryEncoding` and `CategoryDuplicateKey` are typed constants. They used to be untyped strings. Comparisons, `switch` statements and printing still compile. **Migrate:** where you store a category in a `string` or pass it as one, write `string(se.Category)`. - `TypeError.GoType` is now a `reflect.Type`. It used to be a `string`. It is nil when no type was involved, so two types with one name in different packages can be told apart and a type can be compared: `te.GoType == reflect.TypeFor[time.Duration]()`. Messages print the type as before. **Migrate:** where you used the string, call `te.GoType.String()` after checking for nil. - `KnownFields`, `MergeKeys` and `YAML11Strings` are now `func(...bool) Option`, and `Document.Get`, `Document.Decode` and `Node.Decode` now take `...Option` (see Added). Every call that compiled before still compiles. A method value or a function-typed variable no longer matches, such as `var f func(string, any) error = doc.Get`, and neither does an interface that lists the old methods. **Migrate:** wrap the call in a function literal, such as `func(p string, v any) error { return doc.Get(p, v) }`. - `Node.Find` now returns an error matching `ErrAmbiguousKey` for a key written more than once in its mapping, and `Node.Lookup` now returns `false` for it. Both used to return the first entry. `Document.Get` and every edit already refused a pointer through such a key. **Migrate:** read every entry with `Entries`, and use `Find` to learn why a lookup failed ([Reading while you edit](https://recursiveai.org/oneyaml/go/docs/editing/#reading-while-you-edit)). - Under a `MaxInputBytes` bound (`Untrusted()` sets one), `Set`, `Rename`, `SetHeadComment` and `SetLineComment` now refuse an edit that would make the text longer than the bound. Before, only `ApplyPatch` and `ApplyMergePatch` were held to it. The error is an `*EditError`, and `errors.As` finds the `*LimitError` inside it. The document is unchanged, and an edit that makes the text shorter is always allowed. For a UTF-16 or UTF-32 document, these edits and the patches now count the size in the encoding the document is written in, byte order mark included; patches used to count the UTF-8 size. Even with no bound set, an edit or patch is refused when the text would pass 2 GiB, the most a `Document` can address. **Migrate:** pass a larger bound when you parse, with `Limits(oneyaml.LimitSet{MaxInputBytes: n})` after `Untrusted()` ([Limits](https://recursiveai.org/oneyaml/go/docs/limits/#each-limit)). - `Parse`, `Unmarshal`, the `Decoder` and the compat packages now refuse two kinds of invalid YAML with a `*SyntaxError`. Version 0.3.0 loaded both: - Inside a flow collection, a flow collection that touches the `:` of a plain or empty key, as in `{a:{}}`, `[a:[]]` and `{:{0}}`. After a quoted key (`{"a":{}}`) it is still allowed. - A verbatim tag that is neither a local tag nor a URI that starts with a scheme, as in `!`, `!` and `!<$:?>`. **Migrate:** put a space after the colon (`{a: {}}`), and write such a tag as a local tag (`!`), a full URI (`!`) or a shorthand tag (`!name`). - `compat/yamlv3`: `Marshal` now returns an error naming the type and the method for a `MarshalYAML` method that is not `MarshalYAML() (interface{}, error)`. It used to skip the method and write the value field by field, as yaml.v3 does. The `oneyaml` package already refused such a method. **Migrate:** give the method the signature `MarshalYAML() (interface{}, error)`, or remove it. - `compat/yamlv3`: `Marshal` now returns an error naming the type for a value of the `Node` type of `gopkg.in/yaml.v3` or `go.yaml.in/yaml/v3`. This covers such a value given directly, held in a field, or returned by a `MarshalYAML` method. The adapter used to write the node's Go fields (`kind: 8`, `style: 0`, ...), where yaml.v3 writes the YAML the node holds. **Migrate:** build the adapter's own `yaml.Node`, or write that value with yaml.v3 ([Migrating from yaml.v3](https://recursiveai.org/oneyaml/go/docs/migrate-yaml-v3/#check-after-the-swap)). - `compat/yamlv3`: `Unmarshal` and `Decode` now return an error naming the type when the target is the `Node` type of `gopkg.in/yaml.v3` or `go.yaml.in/yaml/v3`, held directly, in a field, in a slice or map, or behind a pointer. They used to return no error and leave the node empty. A null value still leaves a pointer to it nil. **Migrate:** decode into the adapter's `yaml.Node`. ### Added - Sentinel errors for every error kind, so a single `errors.Is` call tells kinds apart. `*SyntaxError` matches `ErrSyntax`, and also `ErrEncoding` or `ErrDuplicateKey` for those categories. `*LimitError` matches `ErrLimit`, and also the sentinel of its limit, named like the `LimitSet` field: `ErrMaxDepth`, `ErrMaxAliasNodes`, `ErrMaxAliasBytes`, `ErrMaxInputBytes`, `ErrMaxScalarBytes`, `ErrMaxDocuments`, `ErrMaxNumberDigits` and `ErrMaxPatchWorkRatio`, also through an `*EditError` that wraps it ([Limits](https://recursiveai.org/oneyaml/go/docs/limits/#each-limit)). - The type `Limit` and its constants `LimitMaxDepth`, `LimitMaxAliasNodes`, `LimitMaxAliasBytes`, `LimitMaxInputBytes`, `LimitMaxScalarBytes`, `LimitMaxDocuments`, `LimitMaxNumberDigits` and `LimitMaxPatchWorkRatio`, for `LimitError.Limit`. Also the field `LimitError.What`, and the type `Category` for `SyntaxError.Category` (see Breaking). - `oneyaml.Pointer(tokens...)` builds an RFC 6901 pointer from keys and indices, escaping `~` and `/` in each. For example, `Pointer("metadata", "labels", "app.kubernetes.io/name")` is `/metadata/labels/app.kubernetes.io~1name`. Use it instead of joining keys with `"/"`. - `Document.Get`, `Document.Decode` and `Node.Decode` take options for one read, applied on top of the options the Document was parsed with. For example, `doc.Get("/spec", &spec, oneyaml.KnownFields())` reads one part strictly. `KnownFields`, `MergeKeys`, `Limits`, `Untrusted` and `NoLimits` apply to a read; other options are ignored. The options also hold for the decodes an `UnmarshalYAML` hook makes during that read. A bound given to a read of a Document replaces the Document's bound in both directions, so `NoLimits()` on a read of a Document parsed with `Untrusted()` lifts its bounds for that read. Pass such options only for input you trust ([Reading values back](https://recursiveai.org/oneyaml/go/docs/editing/#reading-values-back)). Inside an `UnmarshalYAML` hook it is different: options given to `Node.Decode` may only tighten the limits of the read that called the hook, never loosen them. For each bound the stricter of the caller's and the option's applies, so a type's hook cannot undo a caller's `Untrusted()`. `KnownFields` and `MergeKeys` given there apply as given. - `KnownFields`, `MergeKeys` and `YAML11Strings` take an optional bool, so `KnownFields(false)` turns the option off again, and a later option overrides an earlier one. Calls without an argument behave as before. - `Node` is now documented as comparable: two Nodes are equal when they are the same node of the same state of the same Document. A Node read before an edit is never equal to the one read after it. ### Changed - `LimitError.Path` of a limit hit by a decode inside an `UnmarshalYAML` hook is now the pointer in the whole document, like the path of a `*TypeError`. This holds when the hook wraps the error, as with `fmt.Errorf("...: %w", err)` or `errors.Join`, when hooks are nested and each one wraps it, and when an outer hook decoded a child of its `Node`. It used to be empty. The read does not change the error the hook returned: `errors.As` on the read's error finds a copy that carries the whole-document path. A hook may therefore keep that error and return it from later reads, even on several goroutines at once, and those reads return it as it is. - A type error found by a decode inside an `UnmarshalYAML` hook (a `Node.Decode`, or the function a func-style hook is given) is now reported where the value is in the input, with every mismatch the hook's decode found, as for a read without the hook. Its path is the hook's path followed by the path inside the node the hook decoded, so a hook that decodes a child node reports that child's path. Before, only the first mismatch was reported, at the first key of the hooked mapping, and a child's path was cut off at the hook. A bad value on line 7 could be reported on line 5. This holds at any depth of nested hooks and on every read path. - The `Unmarshaler` documentation now says how a `Node` that `UnmarshalYAML` keeps may be used after the method returns: later, from one goroutine at a time, but not from another goroutine at the moment the method returns. A kept Node goes on reading its own subtree, whichever read gave it. One kept from `Unmarshal` reads the byte slice you passed in, so leave that slice unchanged while you keep the Node. One kept from a `Document` read becomes stale when the `Document` is edited. - An error that a hook returns from reading other text, such as an `Unmarshal` of its own input, is now reported at the hook's value with the hook's type as `GoType`. The position and path inside the other text stay in the message: `line 1, column 4: /a: line 8, column 4: /x: ...`. Before, its path was joined onto the hook's (`/a/x`) and its own line and column were lost. - A type whose `UnmarshalYAML` method has a form that cannot be called, and a `Node` used as a decode target, are now collected like a value that does not fit. The value is skipped, the rest of the document is read, and the `*TypeError` lists every mismatch. The first such value used to stop the read. - `LimitError.Path` is now set for limits that are hit at a value while it is read: alias expansion, scalar size, number digits and the fixed nesting bound of typed values. It was already set for patches. It stays empty for the limits `Parse` reports and for input size, document count and nesting that the parser stops at. `LimitError.Error` shortens a `Path` longer than 512 bytes, as `TypeError.Error` does. - Limit error messages name the option in its new form, or none (see Breaking), and give the path when there is one. The document-count limit from `Parse` now says "documents per stream", as the `Decoder`'s does. ### Fixed - An alias refers to the latest anchor of its name written before it, even when that anchor is inside an earlier node with the same anchor name. In `a: &b {s: &b 1, t: *b}`, `t` is `1`. This document used to be refused with "refers to a node that contains it". In `a: &b {se: &b 1}` followed by `copy: *b`, `copy` is `1`; it used to be `{se: 1}`. Every reader agrees, the compat packages included. - An unknown field found under `KnownFields()` by a decode inside an `UnmarshalYAML` hook now matches `ErrUnknownField`. It did not. - Edits to a later document of a stream that starts with a byte order mark: `Set` of a new key and `SetHeadComment` on its first entry or root work, and the mark stays at the start of the line. They used to fail with "edit could not be applied" or `ErrUnsupportedEdit`. - `Set` of the empty value of an explicit key (`? key`) followed by a `: value` line that belongs to an outer mapping writes the value under the key. It used to fail with "edit could not be applied". - `compat/yamlv3` reads comments onto the same nodes as yaml.v3 in more layouts: - A comment on a dash line with no value (`- # note`) goes to the first key or scalar of the item, or to a flow collection that comes first in it. It used to be moved above the item. - The comments of an empty last item (`- # note` at the end of the text) are the document's foot comment. They used to be dropped. - A foot comment of the item before such a dash line stays with that item. It used to be dropped. - A comment line just before the closing bracket of a flow collection is the foot comment of the last item or key. It used to be dropped. - The comments after a flow collection that ends on the first line of the text (`[a, b]` or `a: [1, 2]`, then `# note`) are the document's foot comment. They used to go to the collection, or to its key. - `compat/yamlv3` writes text that reads back: - A folded scalar (`>`) always reads back as the value written. Some values used to read back with a line break turned into a space or an extra line break, such as a line followed by an indented line. In some of these cases yaml.v3's own text does not read back. - A foot comment held by a block collection that is an item of a sequence is written once, after that collection. It used to be dropped. - A blank line between the lines of a head comment is written without indentation, as yaml.v3 writes it. - `compat/yamlv3` reads each comment line once, and tells one line from another by where it stands in the text, not by its words, so two comments with the same words are both kept. When several sequences start on one dash line (`- # a`, then `- # b`, then a mapping), the comment lines above the dashes were given to more than one node, and writing the result repeated them, adding a copy on every round trip. Comments at the end of a document are no longer dropped when the key that would take them has a foot comment already, and a comment block under `key:` followed by a blank line and another block heads the first key of the value, as in yaml.v3. ## [0.3.0] - 2026-10-16 ### Added - `Node.Find(key)` is the checked form of `Lookup`: it returns the value with a `nil` error, or an error that says why there is none. The error matches `ErrNotFound` for an absent key (or a node that is not a mapping), `ErrStaleNode` for a node read before an edit, and `ErrAmbiguousKey` when a merge brings the key in from two places of one source, and it is a `*LimitError` when a merge search was refused by `MaxAliasNodes`. `Lookup` reports all of these as `false`. See [Reading while you edit](https://recursiveai.org/oneyaml/go/docs/editing/#reading-while-you-edit). - `Node.Err` says why a `Node` reads as zero values: it is `nil` for a node you can read, matches `ErrStaleNode` when the document was edited after the node was read, and matches `ErrNotFound` for the zero `Node`, such as the one a failed `Lookup` returns. `Value` of a stale node is `""`, the same as an empty scalar, so `Err` is the check to make. See [Reading while you edit](https://recursiveai.org/oneyaml/go/docs/editing/#reading-while-you-edit). - With `MergeKeys()`, a pointer also finds the keys a `<<` merge brings into a mapping, with the precedence decoding gives them: `Document.Get`, the patch `test` operation, the `from` of a patch `copy`, `Node.Lookup` and `Node.Find` read them. A `copy` from such a key copies the merged value and leaves the merge source unchanged. `Set` on such a key adds an explicit key that overrides the merged one. Each merge source is searched once, and the keys read count against `MaxAliasNodes`. See [Merge keys](https://recursiveai.org/oneyaml/go/docs/editing/#merge-keys) and the Changed entry below. - `ApplyMergePatch` also reads a merge patch written in YAML, with the same refusals as a YAML `ApplyPatch`. Text that starts with `{` or `[`, or is valid JSON, is still read as JSON. A mapping key that is not a string (`1: 5`, `true: 5`, an empty key) is refused at any depth with its line and column, because merge patch members are named by strings; quote the name to use it. See [Patches written in YAML](https://recursiveai.org/oneyaml/go/docs/patches/#patches-written-in-yaml). ### Changed - **Breaking:** more repeated mapping keys are duplicate-key errors, so some input that used to load is refused: - repeated `.nan` keys, in any spelling (`.nan` and `.NaN` are one key), read into `any`, a `map[float64]T` or another map whose keys are not strings. A map with string keys still compares keys as text; - a repeated key that no field of a struct target has, and a key repeated inside a value the target ignores. `a: 1` twice into a struct with no field `a` used to load; - `compat/yamlv3` reports a key repeated at the top of a mapping its target ignores, as yaml.v3 does. This removes a listed gap. Fix such input by removing the repeated key. The rules are in the [`Unmarshal` reference](https://pkg.go.dev/oneyaml.dev/go#Unmarshal). - **Breaking:** `Parse` refuses a `\u` or `\U` escape that is not a Unicode scalar value, such as a lone surrogate (`"\ud83d"`), a low surrogate before a high one, or a value past `U+10FFFF`, with a `*SyntaxError`. `Parse` used to accept such a document, and only `Get` or `Decode` of the value failed. `Unmarshal` already refused it. Write a valid escape instead: a character outside the Basic Multilingual Plane as an escaped surrogate pair (`"\ud83d\ude00"`), which is now read as one character (see Fixed), or as `\U0001F600`. A lone surrogate is not a character, so replace it, with `\uFFFD` for example, or remove it. - **Breaking:** with `MergeKeys()`, an edit that would change a key only a merge source has is refused with `ErrAliasWrite`, because it would write into the source through an alias: `Remove`, the patch operations `remove`, `replace` and `move`, a merge patch member `null` that would delete it, and any write below such a key or through the merge key itself (`/use/< # Parse YAML into Go structs Read a YAML file into typed Go data: nested structs, a map, an optional section, durations and defaults. `oneyaml.Unmarshal` works like `encoding/json`’s, with `yaml` struct tags. The file: ```yaml # service.yaml name: web image: nginx:1.27 timeout: 30s env: LOG_LEVEL: info PORT: "8080" healthcheck: path: /healthz interval: 10s ``` The program: ```go package main import ( "fmt" "log" "os" "time" oneyaml "oneyaml.dev/go" ) type Service struct { Name string `yaml:"name"` Image string `yaml:"image"` Replicas int `yaml:"replicas,omitempty"` Timeout time.Duration `yaml:"timeout"` Env map[string]string `yaml:"env"` Healthcheck *Healthcheck `yaml:"healthcheck"` } type Healthcheck struct { Path string `yaml:"path"` Interval time.Duration `yaml:"interval"` } func main() { src, err := os.ReadFile("service.yaml") if err != nil { log.Fatal(err) } svc := Service{Replicas: 1} // defaults: a missing key keeps the field as it is if err := oneyaml.Unmarshal(src, &svc, oneyaml.KnownFields()); err != nil { log.Fatal(err) } fmt.Printf("%s runs %s x%d, timeout %v\n", svc.Name, svc.Image, svc.Replicas, svc.Timeout) fmt.Println("env:", svc.Env) if h := svc.Healthcheck; h != nil { fmt.Printf("health check: %s every %v\n", h.Path, h.Interval) } // KnownFields: a key the struct doesn't have is an error, not ignored. err = oneyaml.Unmarshal([]byte("name: web\nimgae: nginx\n"), &svc, oneyaml.KnownFields()) fmt.Println(err) } ``` ```text web runs nginx:1.27 x1, timeout 30s env: map[LOG_LEVEL:info PORT:8080] health check: /healthz every 10s oneyaml: line 2, column 1: /imgae: field "imgae" not found in type main.Service ``` ## Good to know - **Defaults:** set them on the value before `Unmarshal`. A key that isn’t in the file leaves its field as it was, so `Replicas` stays 1. A `null` value also leaves a struct field, number or string as it was. - **`KnownFields()`** makes a key the struct doesn’t have an error that names it, with its position. Without it, unknown keys are ignored. - `time.Duration` reads its `String` form (`30s`, `1m30s`), `time.Time` a timestamp, and `*big.Int` any integer. - `PORT: "8080"` is quoted, so it is a string. Plain `8080` is an integer; reading it into a `string` field is a `*TypeError`, not a silent conversion. - Types with an `UnmarshalYAML` method, `encoding.TextUnmarshaler` or `json.Unmarshaler` read themselves, so types written for `encoding/json` (Kubernetes quantities, say) need no YAML code. When the shape isn’t known ahead of time, read nodes from a `Document`: [read YAML without a struct](https://recursiveai.org/oneyaml/go/cookbook/read-yaml-without-a-struct/). # Read YAML without a struct When you don’t have a struct for the data, parse it into a `Document` and read values by JSON Pointer, or walk its nodes. A node also gives you its position and its comments. ```go package main import ( "fmt" "log" oneyaml "oneyaml.dev/go" ) func main() { doc, err := oneyaml.Parse([]byte(`server: host: 0.0.0.0 port: 8080 # behind the load balancer features: [auth, metrics] limits: cpu: 0.5 memory: 512Mi `)) if err != nil { log.Fatal(err) } // Read one value into a Go type, by JSON Pointer. var port int if err := doc.Get("/server/port", &port); err != nil { log.Fatal(err) } fmt.Println("port:", port) // Or walk the nodes: values, comments and positions. root := doc.Root() server, _ := root.Lookup("server") p, _ := server.Lookup("port") line, col := p.Position() fmt.Printf("port %s at %d:%d, comment %q\n", p.Value(), line, col, p.LineComment()) features, _ := root.Lookup("features") for f := range features.Items() { fmt.Println("feature:", f.Value()) } limits, _ := root.Lookup("limits") for k, v := range limits.Entries() { fmt.Printf("limit %s = %s\n", k.Value(), v.Value()) } // A missing path is ErrNotFound, never a panic. var tls bool fmt.Println(doc.Get("/server/tls", &tls)) } ``` ```text port: 8080 port 8080 at 3:9, comment "# behind the load balancer" feature: auth feature: metrics limit cpu = 0.5 limit memory = 512Mi oneyaml: get "/server/tls": path not found ``` ## Good to know - `doc.Get(ptr, &v)` decodes the node at a JSON Pointer into any Go value, with the same rules as `Unmarshal`. A missing path returns an error that matches `oneyaml.ErrNotFound` with `errors.Is`. - `Lookup` finds a key in a mapping; `Find` does the same but returns an error. `Entries()` and `Items()` are range-over-func iterators, in file order. - `Position()` is the 1-based line and column, `Span()` the byte range, and `HeadComment()` and `LineComment()` the comments above and after the node. - To read into plain Go values instead, `Unmarshal` into `any`: a mapping becomes `map[string]any`, a sequence `[]any`, an integer `int`. A longer walk-through, with yaml.v2 and v3 compared: [Golang: read a YAML file without a struct](https://recursiveai.org/oneyaml/articles/golang-read-yaml-without-struct/). # Edit YAML and keep its comments Change a YAML file from Go the way a person would: one value, one list item, one new key, one renamed key. Every comment, blank line and quote elsewhere stays as it was. ```go package main import ( "fmt" "log" oneyaml "oneyaml.dev/go" ) func main() { doc, err := oneyaml.Parse([]byte(`# 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 `)) if err != nil { log.Fatal(err) } must := func(err error) { if err != nil { log.Fatal(err) } } must(doc.Set("/server/port", 9090)) // its trailing comment stays must(doc.Remove("/features/1")) // its comment goes with it must(doc.Set("/server/timeout", "30s")) // a new key, laid out like its siblings must(doc.SetLineComment("/server/timeout", "per request")) // a comment of its own must(doc.Rename("/server/host", "bind")) // a key renamed in place fmt.Print(doc) } ``` ```text # App settings, edited by hand and by tools. server: bind: 0.0.0.0 port: 9090 # behind the load balancer timeout: 30s # per request # Feature flags features: - auth - metrics ``` ## Good to know - **Each value keeps its style.** `Set` writes in the old value’s style and falls back to the nearest style that can hold the new value, for that value only. Reading the document back always gives exactly the value you set. - **Comments:** a comment after a value stays when the value changes, and a removed item takes its own comment with it. `SetHeadComment` writes the comment above an entry, `SetLineComment` the one after its value; an empty text removes it. - **New members** go at the end of their mapping and copy the style of their siblings. - **Refusals are errors, not damage.** `Set` refuses to write below an alias or to a key that occurs twice. On any error the document is unchanged and the error is an `*EditError`. - A new plain `yes` is a string under YAML 1.2 but a boolean to a YAML 1.1 reader. If YAML 1.1 programs also read the file, parse it with `YAML11Strings()` so such strings are quoted. Every rule is in [Editing](https://recursiveai.org/oneyaml/go/docs/editing/). To change many values from a JSON document, use a patch: [merge or patch YAML](https://recursiveai.org/oneyaml/go/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. ```go package main import ( "errors" "fmt" "log" oneyaml "oneyaml.dev/go" ) func main() { doc, err := oneyaml.Parse([]byte(`# values.yaml image: repository: registry.example.com/web tag: "1.4.2" # set by CI replicas: 2 resources: limits: cpu: 500m memory: 256Mi `)) if err != nil { log.Fatal(err) } // An environment overlay as a merge patch: objects merge, null deletes. overlay := `{"replicas": 5, "resources": {"limits": {"cpu": null, "memory": "1Gi"}}}` if err := doc.ApplyMergePatch([]byte(overlay)); err != nil { log.Fatal(err) } fmt.Print(doc) // A JSON Patch with a guard, built in Go: `test` must pass or nothing changes. bump := []oneyaml.PatchOp{ oneyaml.PatchTest("/image/tag", "1.4.2"), oneyaml.PatchReplace("/image/tag", "1.5.0"), } if err := doc.Apply(bump...); err != nil { log.Fatal(err) } var tag string _ = doc.Get("/image/tag", &tag) fmt.Println("tag now", tag) // Applied again, the test fails and the document is left as it was. err = doc.Apply(bump...) var ee *oneyaml.EditError fmt.Println("second run refused:", errors.As(err, &ee), err) } ``` ```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: true oneyaml: patch operation 0 (test "/image/tag"): patch test failed ``` ## Good to know - **Atomic.** If any operation fails, nothing is applied and the error is an `*EditError` that names the operation. - **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, and a write through an alias is refused. - `ApplyPatch` takes RFC 6902 JSON text; `Apply` takes `PatchOp` values built with `PatchAdd`, `PatchRemove`, `PatchReplace`, `PatchMove`, `PatchCopy` and `PatchTest`. The work of a patch is bounded unless you pass `NoLimits()`. Every rule is in [Patches](https://recursiveai.org/oneyaml/go/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, with oneyaml and the standard library’s `encoding/json`. ```go package main import ( "encoding/json" "fmt" "log" oneyaml "oneyaml.dev/go" ) func main() { // YAML to JSON: read into any, write with encoding/json. var v any err := oneyaml.Unmarshal([]byte(`name: web ports: [80, 443] tls: true owner: ~ `), &v) if err != nil { log.Fatal(err) } out, err := json.MarshalIndent(v, "", " ") if err != nil { log.Fatal(err) } fmt.Println(string(out)) // JSON to YAML: JSON is valid YAML 1.2, so read it and write YAML. var w any if err := oneyaml.Unmarshal([]byte(`{"name": "web", "ports": [80, 443], "env": {"LOG": "info"}}`), &w); err != nil { log.Fatal(err) } y, err := oneyaml.Marshal(w) if err != nil { log.Fatal(err) } fmt.Print(string(y)) } ``` ```text { "name": "web", "owner": null, "ports": [ 80, 443 ], "tls": true } env: LOG: info name: web ports: - 80 - 443 ``` ## Good to know - Read into `any`: a mapping becomes `map[string]any`, a sequence `[]any`, an integer `int`, a float `float64` and `null` `nil`, all of which `encoding/json` writes. - Go maps have no order, so both `encoding/json` and `Marshal` write their keys sorted. To keep the file’s order, read into a struct instead. - A mapping with a key that isn’t a string becomes `map[any]any`, which `encoding/json` can’t write. Values JSON can’t hold, such as `.nan`, fail at `json.Marshal`. - JSON is valid YAML 1.2, so `Unmarshal` reads JSON text directly. - For typed data, skip `any`: `Unmarshal` into your struct and `json.Marshal` it. # Read Kubernetes manifests and multi-document YAML A Kubernetes manifest often holds several documents separated by `---`. Read each one into a struct with a `Decoder`, or edit one of them in a `Document` 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: ```go package main import ( "bytes" "errors" "fmt" "io" "log" "os" oneyaml "oneyaml.dev/go" ) type Object struct { APIVersion string `yaml:"apiVersion"` Kind string `yaml:"kind"` Metadata struct { Name string `yaml:"name"` } `yaml:"metadata"` } func main() { src, err := os.ReadFile("app.yaml") if err != nil { log.Fatal(err) } // Typed: a Decoder reads one document per call, until io.EOF. dec := oneyaml.NewDecoder(bytes.NewReader(src)) for { var obj Object err := dec.Decode(&obj) if errors.Is(err, io.EOF) { break } if err != nil { log.Fatal(err) } fmt.Printf("%s %s (%s)\n", obj.Kind, obj.Metadata.Name, obj.APIVersion) } // Lossless: edit the Deployment, keep everything else byte for byte. stream, err := oneyaml.Parse(src) if err != nil { log.Fatal(err) } for i := 0; i < stream.Len(); i++ { doc := stream.Doc(i) var kind string if doc.Get("/kind", &kind) == nil && kind == "Deployment" { if err := doc.Set("/spec/template/spec/containers/0/image", "registry.example.com/web:1.5.0"); err != nil { log.Fatal(err) } } } fmt.Print(stream) } ``` ```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 - `Decoder.Decode` reads one document per call and returns `io.EOF` after the last. `Unmarshal` reads only the first document. - A parsed `Document` holds the whole stream. `Len()` counts its documents, and `Doc(i)` is a view of one that reads and edits only that document. - For manifests from outside, use `Untrusted()`: it also bounds input size, documents per stream and scalar length ([read untrusted YAML safely](https://recursiveai.org/oneyaml/go/cookbook/untrusted-input/)). - Kubernetes code built on `sigs.k8s.io/yaml` can switch to `oneyaml.dev/go/compat/k8syaml` with one import change ([migration guide](https://recursiveai.org/oneyaml/go/docs/migrate-sigs-k8s-yaml/)). # Report errors with line and column Validate a YAML file and say exactly what’s wrong and where. oneyaml’s errors are typed: match them with `errors.As` and read the position from their fields. ```go package main import ( "errors" "fmt" oneyaml "oneyaml.dev/go" ) type Config struct { Port uint16 `yaml:"port"` Debug bool `yaml:"debug"` Workers int `yaml:"workers"` } func main() { // Malformed YAML: a *SyntaxError with a line, a column and a category. err := oneyaml.Unmarshal([]byte("name: web\nports: [80, 443\n"), new(any)) var se *oneyaml.SyntaxError if errors.As(err, &se) { fmt.Printf("%d:%d %s: %s\n", se.Line, se.Column, se.Category, se.Msg) } // A duplicate key says where the first one was. err = oneyaml.Unmarshal([]byte("name: web\nname: api\n"), new(any)) if errors.As(err, &se) { fmt.Printf("%d:%d %q, first at %d:%d\n", se.Line, se.Column, se.Key, se.FirstLine, se.FirstColumn) } // Values that don't fit: every mismatch, each with its path and position. var cfg Config err = oneyaml.Unmarshal([]byte("port: 99999\ndebug: yes\nworkers: 4\n"), &cfg) var te *oneyaml.TypeError if errors.As(err, &te) { for _, e := range te.All() { fmt.Printf("%d:%d %s: %s\n", e.Line, e.Column, e.Path, e.Msg) } } fmt.Println("the rest is still read: workers =", cfg.Workers) } ``` ```text 3:1 syntax: expected ',' or ']' after an entry of the flow sequence opened at line 2, column 8 (is its closing ']' missing?) 2:1 "name", first at 1:1 1:7 /port: value `99999` overflows uint16 2:8 /debug: cannot unmarshal !!str `yes` into bool the rest is still read: workers = 4 ``` ## Good to know - `*SyntaxError` is YAML that isn’t well formed. `Line` and `Column` are 1-based, counted in characters; `Offset` is the byte offset. Switch on `Category`, not on `Msg`. - For a duplicate key, `Key`, `FirstLine` and `FirstColumn` say which key and where it first appeared. - **A type mismatch doesn’t stop the read.** The value that doesn’t fit is skipped and the rest is read. The `*TypeError` describes the first mismatch and `All()` lists every one, each with an RFC 6901 `Path`. - Limit errors are a `*LimitError` naming the `Limit` that was hit ([read untrusted YAML safely](https://recursiveai.org/oneyaml/go/cookbook/untrusted-input/)). Every error matches its sentinel with `errors.Is`: `ErrSyntax`, `ErrLimit`, `ErrNotFound` and the others. - Error messages are written for people and may change in a minor release. Match errors by type, sentinel and constant. # 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 pass `MergeKeys()`. ```go package main import ( "fmt" "log" oneyaml "oneyaml.dev/go" ) const src = `defaults: &defaults image: nginx:1.27 replicas: 2 web: <<: *defaults replicas: 3 worker: *defaults ` type Svc struct { Image string `yaml:"image"` Replicas int `yaml:"replicas"` } func main() { // Aliases are expanded on read; `<<` is a plain key in YAML 1.2. var plain map[string]any if err := oneyaml.Unmarshal([]byte(src), &plain); err != nil { log.Fatal(err) } fmt.Println("worker:", plain["worker"]) fmt.Println("web without merge keys:", plain["web"]) // MergeKeys reads docker-compose-style files the way YAML 1.1 tools do. var merged map[string]Svc if err := oneyaml.Unmarshal([]byte(src), &merged, oneyaml.MergeKeys()); err != nil { log.Fatal(err) } fmt.Printf("web: %+v\n", merged["web"]) // A Document keeps anchors and aliases as written, and can follow them. doc, err := oneyaml.Parse([]byte(src)) if err != nil { log.Fatal(err) } worker, _ := doc.Root().Lookup("worker") target := worker.Target() image, _ := target.Lookup("image") fmt.Printf("worker is *%s; &%s has image %s\n", worker.AliasName(), target.Anchor(), image.Value()) } ``` ```text worker: map[image:nginx:1.27 replicas:2] web without merge keys: map[<<:map[image:nginx:1.27 replicas:2] replicas:3] web: {Image:nginx:1.27 Replicas:3} worker is *defaults; &defaults has image nginx:1.27 ``` ## Good to know - Keys in the mapping win over merged ones: `web.replicas` is 3, not 2. - **Alias expansion is bounded by default**, so a “billion laughs” file fails with a `*LimitError` instead of exhausting memory ([read untrusted YAML safely](https://recursiveai.org/oneyaml/go/cookbook/untrusted-input/)). - **A `Document` keeps `&` and `*` as written.** `AliasName()` names the anchor an alias refers to, `Target()` follows it, and `Anchor()` is a node’s own anchor. - An edit below an alias is refused with `ErrAliasWrite`, because it would change every place that shares the node. Replacing the alias node itself is allowed. With `MergeKeys`, setting a key that only a `<<` source has adds a key of the mapping’s own. 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 nesting depth, alias expansion, number size and patch work by default; for input you don’t control, add `Untrusted()`. ```go package main import ( "errors" "fmt" "strings" oneyaml "oneyaml.dev/go" ) func main() { // A normal request body reads as usual. var ok map[string]any fmt.Println("ok:", oneyaml.Unmarshal([]byte("name: web\nports: [80]\n"), &ok, oneyaml.Untrusted())) // "Billion laughs": nine levels of aliases, each repeated ten times. var b strings.Builder b.WriteString("l0: &l0 [x, x, x, x, x, x, x, x, x, x]\n") for i := 1; i < 9; i++ { refs := strings.TrimSuffix(strings.Repeat(fmt.Sprintf("*l%d, ", i-1), 10), ", ") fmt.Fprintf(&b, "l%d: &l%d [%s]\n", i, i, refs) } var v any err := oneyaml.Unmarshal([]byte(b.String()), &v, oneyaml.Untrusted()) var le *oneyaml.LimitError if errors.As(err, &le) { fmt.Println("refused:", le.Limit, errors.Is(err, oneyaml.ErrLimit)) } // Deep nesting is refused too, without overflowing the stack. err = oneyaml.Unmarshal([]byte(strings.Repeat("[", 100_000)), &v, oneyaml.Untrusted()) if errors.As(err, &le) { fmt.Println("deep:", le.Limit) } // Size the limits yourself: start from UntrustedLimits and change fields. limits := oneyaml.UntrustedLimits() limits.MaxInputBytes = 1 << 10 err = oneyaml.Unmarshal([]byte(strings.Repeat("a: b\n", 1000)), &v, oneyaml.Limits(limits)) if errors.As(err, &le) { fmt.Println("too big:", le.Limit) } } ``` ```text ok: refused: MaxAliasNodes true deep: MaxDepth too big: MaxInputBytes ``` ## Good to know - **Bounded by default:** nesting depth, alias expansion, the digits of a number and the work of a patch, unless you pass `NoLimits()`. - **Bounded only with `Untrusted()`:** input size, documents per stream and scalar length. Pass `Limits(...)` with your own `LimitSet` to size them; start from `UntrustedLimits()` and write the struct with field names. - A limit error is a `*LimitError` whose `Limit` names the bound, and it matches `oneyaml.ErrLimit` with `errors.Is`. - Your own hooks (`UnmarshalYAML` and the like) run outside these bounds, and the compat packages have no `Untrusted()` mode. - No `unsafe` and no cgo. A panic caused by input is treated as a security bug. Every limit and its value is in [Limits](https://recursiveai.org/oneyaml/go/docs/limits/). # Switch from yaml.v3 with one import `oneyaml.dev/go/compat/yamlv3` is an adapter for the common API of `gopkg.in/yaml.v3`: `Unmarshal`, `Marshal`, `Decoder`, `Encoder` and `yaml.Node` with its comments. Change the import path and your call sites stay; the package name is still `yaml`. ```go // before import "gopkg.in/yaml.v3" // after import yaml "oneyaml.dev/go/compat/yamlv3" ``` ```go package main import ( "fmt" "log" yaml "oneyaml.dev/go/compat/yamlv3" // was: yaml "gopkg.in/yaml.v3" ) type Config struct { Name string `yaml:"name"` Enabled bool `yaml:"enabled"` Tags []string `yaml:"tags"` } func main() { // The same calls as yaml.v3: Unmarshal, Marshal, yaml.Node. var cfg Config if err := yaml.Unmarshal([]byte("name: web\nenabled: true\ntags: [a, b]\n"), &cfg); err != nil { log.Fatal(err) } fmt.Printf("%+v\n", cfg) out, err := yaml.Marshal(cfg) if err != nil { log.Fatal(err) } fmt.Print(string(out)) // Comments on yaml.Node, as in yaml.v3. var n yaml.Node if err := yaml.Unmarshal([]byte("# owner: platform\nname: web # the service\n"), &n); err != nil { log.Fatal(err) } m := n.Content[0] fmt.Printf("key head comment %q, value line comment %q\n", m.Content[0].HeadComment, m.Content[1].LineComment) } ``` ```text {Name:web Enabled:true Tags:[a b]} name: web enabled: true tags: - a - b key head comment "# owner: platform", value line comment "# the service" ``` ## Good to know - The adapter reads YAML 1.1 forms the way yaml.v3 does, so existing files mean what they meant. New code should use the `oneyaml` package, which reads YAML 1.2. - **Known differences:** a `3.5` read into an integer field is a type error instead of being truncated; invalid YAML that yaml.v3 accepts is refused; and where yaml.v3 panics, the adapter returns an error. - An `UnmarshalYAML` that takes gopkg.in/yaml.v3’s own `*yaml.Node`, in a type from another module, can’t be called by the adapter, so reading into that type is an error that names it. The [migration guide](https://recursiveai.org/oneyaml/go/docs/migrate-yaml-v3/#check-after-the-swap) shows how to wrap it. - The adapter always runs with the default limits and has no `Untrusted()` mode. Bound the input size yourself. - For `sigs.k8s.io/yaml`, the adapter is `oneyaml.dev/go/compat/k8syaml` ([migration guide](https://recursiveai.org/oneyaml/go/docs/migrate-sigs-k8s-yaml/)). The checklist and every difference are in [Migrating from yaml.v3](https://recursiveai.org/oneyaml/go/docs/migrate-yaml-v3/). A longer guide to the four routes off the archived module: [yaml.v3 is archived: what to use instead](https://recursiveai.org/oneyaml/articles/yaml-v3-alternative/).