# oneyaml
**serde and lossless, comment-preserving editing for YAML 1.2, in one
crate.** Both run on one parser and one interpretation of the file, so a
typed read and an edit never disagree about what it means. Correct by
default, no `unsafe`, no panics on any input.
[](https://crates.io/crates/oneyaml)
[](https://docs.rs/oneyaml)
[](#minimum-supported-rust-version)
[](#license)
How many times fewer CPU instructions oneyaml executes than each
competitor (the fastest feature-complete serde library for serde reads
and writes, yaml-edit for `Document` parsing and a three-edit script),
on five workloads shaped like real configuration files (higher is
better; hatched bars are workloads where oneyaml is slower):
<!-- 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.1.0/docs/img/bench-summary-dark.svg">
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/recursive-ai-oss/oneyaml-rust/v1.1.0/docs/img/bench-summary-light.svg">
<img alt="Chart of CPU instruction counts, not wall-clock time: how many times fewer instructions oneyaml executes than each competitor (competitor ÷ oneyaml; higher is better; 1.0× is equal), against the fastest feature-complete YAML serde library on each workload for serde read and serde write, and yaml-edit for Document parse and Document edit script (insert + set + remove); one bar per workload for 5 workloads (gha-workflow, compose-merge, helm-values, openapi-100k and k8s-multi). serde read vs serde_yaml_ng and serde_norway: 0.96× to 1.3×; slower on 1 of 5, faster on 4 of 5 (slower: openapi-100k 0.96×). serde write vs serde-saphyr: 1.3× to 1.5×; faster on all 5. Document parse vs yaml-edit: 1.0× to 1.3×; faster on all 5. Document edit script (insert + set + remove) vs yaml-edit: 0.55× to 63×; slower on 1 of 5, faster on 4 of 5 (slower: compose-merge 0.55×)." src="https://raw.githubusercontent.com/recursive-ai-oss/oneyaml-rust/v1.1.0/docs/img/bench-summary-light.svg">
</picture>
[Full results, with wall-clock times and the method →](https://github.com/recursive-ai-oss/oneyaml-rust/blob/v1.1.0/docs/performance.md#results-at-a-glance)
<!-- bench:readme:end -->
## Why oneyaml
- **One parser, one meaning.** serde and the lossless `Document` read
the same events through the same resolver, so a typed read and an edit
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.1.0/crates/bench/tests/consistency.rs)).
- **Edits that keep your file.** `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.1.0/docs/conformance.md#results-classification-and-round-trip)).
An edit changes only what you touch: every comment stays with the
entry it belongs to, by
[numbered rules](https://github.com/recursive-ai-oss/oneyaml-rust/blob/v1.1.0/docs/comment-attachment.md),
and new content is laid out like its neighbours.
- **Correct YAML 1.2.2 by default.** The Core schema reads `yes` as a
string and `0777` as a decimal integer, as the spec says; the Failsafe
and JSON schemas are built in. Every YAML 1.1 behaviour,
[merge keys](#merge-keys-) included, and every leniency is a named
setting, off by default
([known deviations](https://github.com/recursive-ai-oss/oneyaml-rust/blob/v1.1.0/docs/conformance.md#known-deviations)).
<!-- 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.1.0/docs/conformance.md)).
<!-- oracles:conformance:end -->
- **Safe on any input.** Every published crate is
`#![forbid(unsafe_code)]`. Every input gives a value or an error, every
fallible API returns `Result` or `Option`, and parsing is iterative, so
deep nesting can't overflow the stack. Every read enforces resource
limits; for input from the network, use
[`Limits::untrusted()`](#untrusted-input). Parsing, serde, editing,
comments and diagnostics are fuzzed
([crates/fuzz](https://github.com/recursive-ai-oss/oneyaml-rust/blob/v1.1.0/crates/fuzz/README.md))
and tested against `serde_yaml_ng`, saphyr, yaml-rust2, libyaml and
yaml-edit.
- **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](#diagnostics)).
- **Fast, with the losses shown.** oneyaml executes fewer instructions
than each competitor on most workloads in the chart above, and an
edit's cost doesn't grow with the file. It isn't faster everywhere: a
typed read of the OpenAPI workload and edits of the smallest file take
more instructions, and in wall-clock time its typed reads are slower
on most of these workloads, `Document` parsing is level or slightly
slower on some, and yaml-edit edits small files faster
([every loss](https://github.com/recursive-ai-oss/oneyaml-rust/blob/v1.1.0/docs/performance.md#results-at-a-glance)).
The only runtime dependency by default is `serde`
([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.1.0/docs/comparison.md).
## Install
```sh
cargo add oneyaml serde -F serde/derive
```
## Quick start
Read and write typed data with serde, then edit a document in place:
the edit changes only what you touched, and the comment stays.
```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(())
}
```
## Diagnostics
An error says what went wrong, where, and how to fix it. `Display` is one
line for logs; [`Error::render`](https://docs.rs/oneyaml/latest/oneyaml/struct.Error.html#method.render)
prints a rustc-style snippet of your source with a stable `Y####` code,
labels, help, the path to the value, and a suggested fix you can apply:
```rust
use serde::Deserialize;
#[derive(Deserialize)]
struct Config { debug: bool }
fn main() -> Result<(), Box<dyn std::error::Error>> {
// `yes` is a boolean in YAML 1.1 but a string in YAML 1.2.
let src = "debug: yes\n";
let err = oneyaml::from_str::<Config>(src).err().ok_or("expected an error")?;
assert_eq!(err.render(src), r#"error[Y3000]: invalid type: string "yes", expected a boolean; YAML 1.2 reads `yes` as a string (its booleans are `true` and `false`)
--> 1:8
|
1 | debug: yes
| ^^^
|
= help: write `true` instead of `yes`, or read the YAML 1.1 booleans (y/n, yes/no, on/off) as booleans with Settings::yaml11_booleans(true) or the Compat::yaml_1_1() preset
= path: debug
= suggestion (may be incorrect): write the YAML 1.2 boolean `true`: replace `yes` with `true`
"#);
let fix = err.diagnostic().suggestions.first().ok_or("no fix")?;
assert_eq!(fix.apply(src).as_deref(), Some("debug: true\n"));
// A forgotten `]`: the error points at the `[` that is still open
// and offers to close it after `443`.
let src = "ports: [80, 443\nimage: nginx\n";
let err = oneyaml::Document::parse(src).err().ok_or("expected an error")?;
let fix = err.diagnostic().suggestions.first().ok_or("no fix")?;
assert_eq!(fix.apply(src).as_deref(), Some("ports: [80, 443]\nimage: nginx\n"));
Ok(())
}
```
A fix is marked machine-applicable only when applying it can't change
what the rest of the file means; any other fix, like the two above, is
labelled for review. Codes are never reused, and limit errors name the
setting to raise.
[`Document::parse_recovering`](https://docs.rs/oneyaml/latest/oneyaml/struct.Document.html#method.parse_recovering)
keeps going past the first error, so you see a file's errors together,
and the
`miette` feature plugs errors into miette's reports. More on
[diagnostics on `Error`](https://docs.rs/oneyaml/latest/oneyaml/struct.Error.html#diagnostics).
## When not to use oneyaml
- **You need the fastest typed reads in wall-clock time.** oneyaml's
serde reads use fewer instructions on most workloads, but in
wall-clock time they are slower than `serde_yaml_ng` or `serde_norway`
on most of the benchmark workloads
([serde read wall-clock](https://github.com/recursive-ai-oss/oneyaml-rust/blob/v1.1.0/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
(compose-merge in the chart above; see the
[benchmark tables](https://github.com/recursive-ai-oss/oneyaml-rust/blob/v1.1.0/docs/performance.md#results-at-a-glance)).
- **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.1.0/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.1.0/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,
and the guides in the repository cover the rest:
- **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.1.0/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.1.0/docs/migrating-from-serde-yaml.md)
walks through it and lists every remaining difference with its reason.
- **Performance in detail.**
[Results at a glance](https://github.com/recursive-ai-oss/oneyaml-rust/blob/v1.1.0/docs/performance.md#results-at-a-glance)
has the numbers behind the chart as tables (absolute instruction
counts, both ratios, wall-clock time, every workload where oneyaml is
slower and the machine they were measured on); the rest of
[docs/performance.md](https://github.com/recursive-ai-oss/oneyaml-rust/blob/v1.1.0/docs/performance.md)
covers every workload, memory, scaling and how to reproduce it all.
- **Real-world programs.**
[`examples/`](https://github.com/recursive-ai-oss/oneyaml-rust/tree/v1.1.0/crates/oneyaml/examples)
starts with
[`edit_config.rs`](https://github.com/recursive-ai-oss/oneyaml-rust/blob/v1.1.0/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.1.0/crates/oneyaml/examples/k8s_manifest.rs),
a [GitHub Actions workflow](https://github.com/recursive-ai-oss/oneyaml-rust/blob/v1.1.0/crates/oneyaml/examples/gha_workflow.rs)
and a [docker-compose file with merge keys](https://github.com/recursive-ai-oss/oneyaml-rust/blob/v1.1.0/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.1.0/CHANGELOG.md).
## License
Licensed under either of
- Apache License, Version 2.0
([LICENSE-APACHE](https://github.com/recursive-ai-oss/oneyaml-rust/blob/v1.1.0/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.1.0/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.1.0/CONTRIBUTING.md)
and [SECURITY.md](https://github.com/recursive-ai-oss/oneyaml-rust/blob/v1.1.0/SECURITY.md).