oneyaml 1.0.1

oneyaml — one core, one interpretation: serde + lossless editing for YAML 1.2.
Documentation
# oneyaml

**One core, one interpretation: serde and lossless, comment-preserving
editing for YAML 1.2, correct by default and free of panics.**

[![crates.io](https://img.shields.io/crates/v/oneyaml.svg)](https://crates.io/crates/oneyaml)
[![docs.rs](https://img.shields.io/docsrs/oneyaml)](https://docs.rs/oneyaml)
[![MSRV 1.85](https://img.shields.io/badge/MSRV-1.85-blue.svg)](#minimum-supported-rust-version)
[![License: MIT OR Apache-2.0](https://img.shields.io/badge/license-MIT%20OR%20Apache--2.0-blue.svg)](#license)

How many times fewer CPU instructions oneyaml needs than the fastest
feature-complete competitor, for four operations (serde read, serde
write, `Document` parse, a three-edit `Document` script) on five
generated inputs shaped like real configuration files:

<!-- bench:readme:begin -->
<!-- Generated by `bench readme` from docs/performance.md; do not edit. -->
<picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/recursive-ai-oss/oneyaml-rust/v1.0.1/docs/img/bench-summary-dark.svg">
  <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/recursive-ai-oss/oneyaml-rust/v1.0.1/docs/img/bench-summary-light.svg">
  <img alt="Bar chart: how many times fewer CPU instructions oneyaml needs than each competitor (competitor ÷ oneyaml; higher is better; 1.0× is parity). serde read: gha-workflow 1.1× vs serde_yaml_ng, compose-merge 0.79× — slower vs serde_yaml_ng, helm-values 1.3× vs serde_yaml_ng, openapi-100k 0.96× — slower vs serde_norway, k8s-multi 1.1× vs serde_norway. serde write: gha-workflow 1.5× vs serde-saphyr, compose-merge 1.5× vs serde-saphyr, helm-values 1.3× vs serde-saphyr, openapi-100k 1.4× vs serde-saphyr, k8s-multi 1.5× vs serde-saphyr. Document parse: gha-workflow 1.3× vs yaml-edit, compose-merge 1.1× vs yaml-edit, helm-values 1.2× vs yaml-edit, openapi-100k 1.0× vs yaml-edit, k8s-multi 1.0× vs yaml-edit. Document edit script (insert + set + remove): gha-workflow 2.2× vs yaml-edit, compose-merge 0.56× — slower vs yaml-edit, helm-values 7.3× vs yaml-edit, openapi-100k 63× vs yaml-edit, k8s-multi 3.1× vs yaml-edit." src="https://raw.githubusercontent.com/recursive-ai-oss/oneyaml-rust/v1.0.1/docs/img/bench-summary-light.svg">
</picture>

| serde read (typed) | vs | oneyaml Ir/B | competitor Ir/B | × faster |
|---|---|---|---|---|
| gha-workflow (3.1 kB) | serde_yaml_ng | **264.7** | 292.3 | **1.1×** |
| compose-merge (2.4 kB) | serde_yaml_ng | 784.3 | **623.0** | 0.79× — slower |
| helm-values (100 kB) | serde_yaml_ng | **134.2** | 174.7 | **1.3×** |
| openapi-100k (100 kB) | serde_norway | 362.9 | **348.4** | 0.96× — slower |
| k8s-multi (1.1 MB) | serde_norway | **373.1** | 422.1 | **1.1×** |

| serde write (typed) | vs | oneyaml Ir/B | competitor Ir/B | × faster |
|---|---|---|---|---|
| gha-workflow (3.1 kB) | serde-saphyr | **127.7** | 193.5 | **1.5×** |
| compose-merge (2.4 kB) | serde-saphyr | **234.4** | 342.8 | **1.5×** |
| helm-values (100 kB) | serde-saphyr | **32.1** | 43.2 | **1.3×** |
| openapi-100k (100 kB) | serde-saphyr | **217.7** | 311.2 | **1.4×** |
| k8s-multi (1.1 MB) | serde-saphyr | **141.5** | 215.0 | **1.5×** |

| `Document` parse | vs | oneyaml Ir/B | competitor Ir/B | × faster |
|---|---|---|---|---|
| gha-workflow (3.1 kB) | yaml-edit | **319.4** | 418.1 | **1.3×** |
| compose-merge (2.4 kB) | yaml-edit | **471.7** | 514.3 | **1.1×** |
| helm-values (100 kB) | yaml-edit | **167.7** | 196.0 | **1.2×** |
| openapi-100k (100 kB) | yaml-edit | **431.2** | 449.0 | **1.0×** |
| k8s-multi (1.1 MB) | yaml-edit | **417.6** | 435.2 | **1.0×** |

| `Document` edit script (insert + set + remove) | vs | oneyaml Ir per script | competitor Ir per script | × faster |
|---|---|---|---|---|
| gha-workflow (3.1 kB) | yaml-edit | **851,028** | 1,870,315 | **2.2×** |
| compose-merge (2.4 kB) | yaml-edit | 543,910 | **302,513** | 0.56× — slower |
| helm-values (100 kB) | yaml-edit | **1,620,284** | 11,823,172 | **7.3×** |
| openapi-100k (100 kB) | yaml-edit | **596,342** | 37,349,293 | **63×** |
| k8s-multi (1.1 MB) | yaml-edit | **568,328** | 1,786,870 | **3.1×** |

- **× faster** = the competitor's CPU instructions ÷ oneyaml's on the same input (higher is better; below 1.0× oneyaml is slower), counted by valgrind (callgrind): deterministic from run to run, not wall-clock time. Ir/B = instructions per input byte; the edit script counts one script of three edits (insert + set + remove) on the parsed document; lower is better, the better value in bold.
- **Wall-clock** (median, same host) points the same way as the instruction count on 16 of 20 rows; the other way on serde read gha-workflow and helm-values; Document parse compose-merge and k8s-multi.
- **Measured** oneyaml 1.0.1 release tree on 2026-10-06 · AMD Ryzen 9 7950X3D 16-Core Processor · Ubuntu 26.04 LTS (x86_64) · rustc 1.97.1 (8bab26f4f 2026-07-14) · valgrind-3.27.1, against serde_yaml_ng 0.10.0, serde_norway 0.9.42, serde-saphyr 1.3.0, yaml-edit 0.3.2. Serde rows compare with the fastest feature-complete YAML serde library on that row (aliases, multi-document, merge keys), `Document` rows with yaml-edit, the other lossless editor; every output is checked for equivalent work. Inputs are generated in the shape of GitHub Actions, docker-compose, Helm, OpenAPI and multi-document Kubernetes files.
- **Full results** (every workload, memory, scaling) and how to reproduce them: [docs/performance.md](https://github.com/recursive-ai-oss/oneyaml-rust/blob/v1.0.1/docs/performance.md) ([regenerate](https://github.com/recursive-ai-oss/oneyaml-rust/blob/v1.0.1/docs/performance.md#regenerate)).
<!-- bench:readme:end -->

The ratios count instructions, which are deterministic for a given
build: rerunning the same binaries reproduces them, while rebuilding
can shift a library's counts by a few percent, so every table comes
from a single run. They are not wall-clock times; the
**Wall-clock** line under the tables names the rows where the median
wall-clock time on the measurement host points the other way, so don't
read those rows as a speed-up. Every row below 1.0× is a loss and is
labelled *slower*. The clearest losses are typed reads of files that use
merge keys (compose-merge) and the edit script on documents of a few
kilobytes, where a fixed cost per edit outweighs the small document
([where oneyaml wins and loses](https://github.com/recursive-ai-oss/oneyaml-rust/blob/v1.0.1/docs/performance.md#results-at-a-glance)).

Full results, every workload, and how to reproduce →
[docs/performance.md](https://github.com/recursive-ai-oss/oneyaml-rust/blob/v1.0.1/docs/performance.md)

## Correctness and safety

- **YAML 1.2.2, Core schema by default.** `yes` is a string and `0777`
  is a decimal integer, as the spec says; the Failsafe and JSON schemas
  are built in. Every YAML 1.1 behaviour and every leniency is a named
  setting, off by default ([known deviations](https://github.com/recursive-ai-oss/oneyaml-rust/blob/v1.0.1/docs/conformance.md#known-deviations)).
- **Conformance.**
  <!-- oracles:conformance:begin -->
  yaml-test-suite `data-2022-01-17`, 402 cases: **402/402** under the standard criterion (valid 308/308, error 94/94), **401/402** strict (exact events, error prefixes and codes), and 308/308 valid inputs round-trip byte for byte ([evidence, other Rust parsers' scores and how to reproduce](https://github.com/recursive-ai-oss/oneyaml-rust/blob/v1.0.1/docs/conformance.md)).
  <!-- oracles:conformance:end -->
- **Fuzzed and differential-tested.** Coverage-guided fuzzing of parsing,
  serde, editing, comments and diagnostics
  ([crates/fuzz](https://github.com/recursive-ai-oss/oneyaml-rust/blob/v1.0.1/crates/fuzz/README.md)),
  and differential tests against `serde_yaml_ng`, saphyr, yaml-rust2,
  libyaml and yaml-edit.
- **No `unsafe`, no panics.** Every published crate is
  `#![forbid(unsafe_code)]`. Every input gives a value or an error, and
  every fallible API returns `Result` or `Option`. Parsing is iterative, so deep
  nesting can't overflow the stack.
- **Bounded by default.** Every read enforces resource limits; for input
  from the network use [`Limits::untrusted()`](#untrusted-input).

## Install

```sh
cargo add oneyaml serde -F serde/derive
```

## Quick start

```rust
use oneyaml::Document;
use serde::{Deserialize, Serialize};

#[derive(Deserialize, Serialize)]
struct Config { name: String, port: u16 }

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // serde: read and write typed data.
    let cfg: Config = oneyaml::from_str("name: web\nport: 8080\n")?;
    assert_eq!(oneyaml::to_string(&cfg)?, "name: web\nport: 8080\n");

    // Read values from a document.
    let mut doc = Document::parse("server:\n  port: 8080  # public\ntags: [web]\n")?;
    assert_eq!(doc.get_path("server.port").and_then(|n| n.as_u64()), Some(8080));

    // Edit it: everything else, comments included, stays byte for byte.
    doc.set_path("server.port", 9090)?;
    doc.get_mut("tags")?.push("api")?;
    assert_eq!(doc.to_string(), "server:\n  port: 9090  # public\ntags: [web, api]\n");
    Ok(())
}
```

## Why oneyaml

- **One interpretation.** serde and `Document` read the same events
  through the same resolver, so they never disagree about what a file
  means; a test checks that they agree on every corpus file
  ([`consistency.rs`](https://github.com/recursive-ai-oss/oneyaml-rust/blob/v1.0.1/crates/bench/tests/consistency.rs)).
- **Lossless.** `Document::parse(src)?.to_string() == src`, byte for
  byte, on every valid file of the yaml-test-suite
  ([round trip](https://github.com/recursive-ai-oss/oneyaml-rust/blob/v1.0.1/docs/conformance.md#results-classification-and-round-trip)).
- **Comments kept.** Edits keep every comment with the entry it belongs
  to, by
  [numbered rules](https://github.com/recursive-ai-oss/oneyaml-rust/blob/v1.0.1/docs/comment-attachment.md);
  new content is laid out like its neighbours.
- **Correct by default.** YAML 1.2.2 and its Core schema (`yes` is a
  string); YAML 1.1 behaviours are opt-in settings
  ([migrating from `serde_yaml`](https://github.com/recursive-ai-oss/oneyaml-rust/blob/v1.0.1/docs/migrating-from-serde-yaml.md#yaml-11-behaviours)).
- **Errors you can act on.** Every error has a stable `Y####` code, a
  span and a path, and common mistakes come with a suggested fix
  ([diagnostics](https://docs.rs/oneyaml/latest/oneyaml/struct.Error.html#diagnostics)).
- **Minimal dependencies.** `serde` (without `derive`), nothing else by
  default ([feature flags](#feature-flags)).

How oneyaml compares with `serde_yaml_ng`, `serde_norway`, serde-saphyr
and yaml-edit, including where they win:
[docs/comparison.md](https://github.com/recursive-ai-oss/oneyaml-rust/blob/v1.0.1/docs/comparison.md).

## When not to use oneyaml

- **You need the fastest typed reads in wall-clock time.** oneyaml's
  serde reads use fewer instructions on most inputs, but in wall-clock
  time `serde_yaml_ng` and `serde_norway` are as fast or faster on every
  workload of the table above
  ([serde wall-clock](https://github.com/recursive-ai-oss/oneyaml-rust/blob/v1.0.1/docs/performance.md#wall-clock-informational-only-1)).
- **You make a few edits to many small files and edit speed matters.**
  Below a few kilobytes, yaml-edit's edits cost fewer instructions (the
  compose-merge row above).
- **You need something oneyaml doesn't do:** `no_std`, async I/O, schema
  validation, templating, building objects from tags, or editing a file
  too large to hold in memory (`Document` keeps the whole tree; serde
  reads don't).
- **You want `serde_yaml`'s exact behaviour with no review.**
  `Compat::serde_yaml()` gets close, but some differences are deliberate;
  the [migration guide](https://github.com/recursive-ai-oss/oneyaml-rust/blob/v1.0.1/docs/migrating-from-serde-yaml.md#divergences)
  lists every one.
- **You need a long production track record.** `serde_yaml`'s lineage has
  years of it; oneyaml is new.

## Untrusted input

Every entry point enforces resource limits (depth, alias expansion,
document size, scalar length, document count). The default profile suits
configuration files; for input from the network, use
`Limits::untrusted()`:

```rust
use oneyaml::{Limits, Settings, Value};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let settings = Settings::new().limits(Limits::untrusted());
    let small = "a: &a [x, x]\nb: &b [*a, *a]\nc: &c [*b, *b]\n";
    let v: Value = oneyaml::from_str_with(small, &settings)?; // small: fine
    assert_eq!(v["c"][0][1][0].as_str(), Some("x"));

    // "Billion laughs": ten levels of ten aliases each.
    let mut laughs = String::from("l0: &l0 [lol]\n");
    for i in 1..10 {
        let refs = vec![format!("*l{}", i - 1); 10].join(", ");
        laughs.push_str(&format!("l{i}: &l{i} [{refs}]\n"));
    }
    let refused = oneyaml::from_str_with::<Value>(&laughs, &settings);
    assert_eq!(refused.err().map(|e| e.code()), Some(oneyaml::code::ALIAS_BUDGET_EXCEEDED));
    Ok(())
}
```

There are three profiles: `Limits::default()`, `Limits::untrusted()` and
`Limits::unlimited()`, the only one without alias budgets (for input you
trust completely). Exceeding a limit is an error whose help names the
setting to raise. The alias budgets count the nodes and bytes that alias
expansion produces beyond the input, and an alias whose whole expansion
would exceed a budget is refused before it is replayed, so "billion
laughs" fails fast. `Document` never expands aliases, and compatibility
presets never change limits. The values of each profile and how the
budgets are counted are on
[`Limits`](https://docs.rs/oneyaml/latest/oneyaml/struct.Limits.html).

## Merge keys (`<<`)

YAML 1.2 has **no merge keys**: by default `<<` is an ordinary string key,
as the spec says. docker-compose files and many CI configurations rely on
the YAML 1.1 merge-key extension; turn it on with one setting:

```rust
use oneyaml::{Settings, Value};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let src = "base: &b {image: nginx, replicas: 2}\nweb:\n  <<: *b\n  replicas: 3\n";
    let v: Value = oneyaml::from_str_with(src, &Settings::new().merge_keys(true))?;
    assert_eq!(v["web"]["image"].as_str(), Some("nginx"));
    assert_eq!(v["web"]["replicas"].as_u64(), Some(3));
    Ok(())
}
```

`Compat::yaml_1_1()` turns on every YAML 1.1 behaviour, merge keys
included; `Compat::serde_yaml()` leaves them off, as `serde_yaml`'s
deserializer does. Merges through aliases count toward the alias budget.
`Document` keeps `<<` entries as written and edits them like any other
entry; [`examples/compose_merge.rs`](https://github.com/recursive-ai-oss/oneyaml-rust/blob/v1.0.1/crates/oneyaml/examples/compose_merge.rs)
edits a docker-compose file that uses them. Details:
[`Settings::merge_keys`](https://docs.rs/oneyaml/latest/oneyaml/struct.Settings.html#method.merge_keys).

## Going further

The API reference on docs.rs has a worked, tested example for each task:

- **Read and edit a document.**
  [`Document`](https://docs.rs/oneyaml/latest/oneyaml/struct.Document.html)
  walks the core path (parse, look up, edit, write) and lists what an
  edit refuses;
  [`NodeRef`](https://docs.rs/oneyaml/latest/oneyaml/struct.NodeRef.html#reading)
  covers typed reads, spans and `deserialize`;
  [`NodeMut`](https://docs.rs/oneyaml/latest/oneyaml/struct.NodeMut.html#values-keys-and-refusals)
  covers edits in depth, and
  [`Removed`](https://docs.rs/oneyaml/latest/oneyaml/struct.Removed.html)
  covers moving an entry. Paths are strings
  (`"spec.containers[0].image"`) or `path!` values; the syntax, and how
  to quote a key that holds a `.`, are on
  [`IntoPath`](https://docs.rs/oneyaml/latest/oneyaml/trait.IntoPath.html).
- **Keep and edit comments.** Each comment has an owner, set by the
  [comment attachment rulebook](https://github.com/recursive-ai-oss/oneyaml-rust/blob/v1.0.1/docs/comment-attachment.md);
  edits move comments with their owner, and `explain_comment` tells you
  which rule decided. See
  [comments on `Document`](https://docs.rs/oneyaml/latest/oneyaml/struct.Document.html#comments).
- **Multi-document files, anchors and aliases.** A file with several
  documents is a
  [`Stream`](https://docs.rs/oneyaml/latest/oneyaml/struct.Stream.html),
  read and edited one document at a time (or iterated with serde).
  Editing an anchored node is seen through every alias; see
  [anchors and aliases](https://docs.rs/oneyaml/latest/oneyaml/struct.Document.html#anchors-and-aliases).
- **Diagnostics and recovering parses.** Every error has a `Y####` code
  ([`code`](https://docs.rs/oneyaml/latest/oneyaml/code/index.html)), a
  span, a path and, for common mistakes, a fix you can apply; see
  [diagnostics on `Error`](https://docs.rs/oneyaml/latest/oneyaml/struct.Error.html#diagnostics)
  and [`Render`](https://docs.rs/oneyaml/latest/oneyaml/trait.Render.html).
  [`Document::parse_recovering`](https://docs.rs/oneyaml/latest/oneyaml/struct.Document.html#method.parse_recovering)
  never fails, and you can still read and edit everything outside the
  broken regions.
- **Serialize and configure.**
  [`to_string`](https://docs.rs/oneyaml/latest/oneyaml/fn.to_string.html)
  and `to_writer` write any `Serialize`, quoting every string that some
  reader would read as something else. Every entry point has a `*_with`
  twin that takes an explicit
  [`Settings`](https://docs.rs/oneyaml/latest/oneyaml/struct.Settings.html)
  (there are no globals): the schema, YAML 1.1 behaviours, duplicate-key
  handling, output layout
  ([`Emit`](https://docs.rs/oneyaml/latest/oneyaml/struct.Emit.html)) and
  resource limits
  ([`Limits`](https://docs.rs/oneyaml/latest/oneyaml/struct.Limits.html)).
- **Migrate from `serde_yaml`.** The entry points keep `serde_yaml`'s
  names (`from_str`, `from_slice`, `from_reader`, `to_string`,
  `to_writer`, `from_value`, `to_value`, `Value`, `Mapping`,
  `Deserializer`, `Serializer`), so migrating starts with a `use` swap,
  and `Compat::serde_yaml()` reads and writes as `serde_yaml` does where
  YAML 1.2 allows. The
  [migration guide](https://github.com/recursive-ai-oss/oneyaml-rust/blob/v1.0.1/docs/migrating-from-serde-yaml.md)
  walks through it and lists every remaining difference with its reason.
- **Real-world programs.**
  [`examples/`](https://github.com/recursive-ai-oss/oneyaml-rust/tree/v1.0.1/crates/oneyaml/examples)
  starts with
  [`edit_config.rs`](https://github.com/recursive-ai-oss/oneyaml-rust/blob/v1.0.1/crates/oneyaml/examples/edit_config.rs)
  (the quick start as a program), then edits a
  [Kubernetes manifest](https://github.com/recursive-ai-oss/oneyaml-rust/blob/v1.0.1/crates/oneyaml/examples/k8s_manifest.rs),
  a [GitHub Actions workflow](https://github.com/recursive-ai-oss/oneyaml-rust/blob/v1.0.1/crates/oneyaml/examples/gha_workflow.rs)
  and a [docker-compose file with merge keys](https://github.com/recursive-ai-oss/oneyaml-rust/blob/v1.0.1/crates/oneyaml/examples/compose_merge.rs),
  checking every comment's owner before and after. Run one with
  `cargo run -p oneyaml --example k8s_manifest`.

## Feature flags

All are off by default; without `miette` and `serde_json`, the only
third-party runtime dependency is `serde`.

- **`miette`**: `Error` implements `miette::Diagnostic` (code, severity,
  labels, help with the suggested fixes, and a documentation URL per
  code); `DiagnosticReport` does the same for a `Diagnostic`. Adds
  `miette` 7 without its default features (it brings `cfg-if` and
  `unicode-width`).
- **`serde_json`**: `TryFrom` conversions between `Value` and
  `serde_json::Value` in both directions, and `Value::to_json_with`. What
  has no JSON form (tags, non-string keys, `.nan`/`.inf`, integers beyond
  64 bits, nesting past 128 levels) is an error with its path, never a
  silent coercion; the full table is on `Value::to_json_with`. Adds
  `serde_json` with only its `std` feature.
- **`__internal-checks`** (unstable, not covered by semver): test-only
  consistency checks used by this crate's own test suites and fuzzers.
  Never enable it in a library or application.

## Minimum supported Rust version

Rust **1.85** (edition 2024), for `oneyaml`, `oneyaml-core` and
`oneyaml-syntax`. The MSRV is rolling: it may move to any stable release
at least 12 months old, only in a minor release, and every move is
announced in the
[changelog](https://github.com/recursive-ai-oss/oneyaml-rust/blob/v1.0.1/CHANGELOG.md).

## License

Licensed under either of

- Apache License, Version 2.0
  ([LICENSE-APACHE](https://github.com/recursive-ai-oss/oneyaml-rust/blob/v1.0.1/LICENSE-APACHE) or
  <https://www.apache.org/licenses/LICENSE-2.0>)
- MIT license
  ([LICENSE-MIT](https://github.com/recursive-ai-oss/oneyaml-rust/blob/v1.0.1/LICENSE-MIT) or
  <https://opensource.org/licenses/MIT>)

at your option.

Unless you explicitly state otherwise, any contribution intentionally
submitted for inclusion in oneyaml by you, as defined in the Apache-2.0
license, shall be dual licensed as above, without any additional terms
or conditions. See
[CONTRIBUTING.md](https://github.com/recursive-ai-oss/oneyaml-rust/blob/v1.0.1/CONTRIBUTING.md)
and [SECURITY.md](https://github.com/recursive-ai-oss/oneyaml-rust/blob/v1.0.1/SECURITY.md).