Expand description
§oneyaml
One core, one interpretation: serde and lossless, comment-preserving editing for YAML 1.2, correct by default and free of panics.
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:
| 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),
Documentrows 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 (regenerate).
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).
Full results, every workload, and how to reproduce → docs/performance.md
§Correctness and safety
- YAML 1.2.2, Core schema by default.
yesis a string and0777is 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). - Conformance.
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). - Fuzzed and differential-tested. Coverage-guided fuzzing of parsing,
serde, editing, comments and diagnostics
(crates/fuzz),
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 returnsResultorOption. 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().
§Install
cargo add oneyaml serde -F serde/derive§Quick start
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
Documentread 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). - Lossless.
Document::parse(src)?.to_string() == src, byte for byte, on every valid file of the yaml-test-suite (round trip). - Comments kept. Edits keep every comment with the entry it belongs to, by numbered rules; new content is laid out like its neighbours.
- Correct by default. YAML 1.2.2 and its Core schema (
yesis a string); YAML 1.1 behaviours are opt-in settings (migrating fromserde_yaml). - 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). - Minimal dependencies.
serde(withoutderive), nothing else by default (feature flags).
How oneyaml compares with serde_yaml_ng, serde_norway, serde-saphyr
and yaml-edit, including where they win:
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_ngandserde_norwayare as fast or faster on every workload of the table above (serde wall-clock). - 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 (Documentkeeps 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 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():
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.
§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:
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
edits a docker-compose file that uses them. Details:
Settings::merge_keys.
§Going further
The API reference on docs.rs has a worked, tested example for each task:
- Read and edit a document.
Documentwalks the core path (parse, look up, edit, write) and lists what an edit refuses;NodeRefcovers typed reads, spans anddeserialize;NodeMutcovers edits in depth, andRemovedcovers moving an entry. Paths are strings ("spec.containers[0].image") orpath!values; the syntax, and how to quote a key that holds a., are onIntoPath. - Keep and edit comments. Each comment has an owner, set by the
comment attachment rulebook;
edits move comments with their owner, and
explain_commenttells you which rule decided. See comments onDocument. - Multi-document files, anchors and aliases. A file with several
documents is a
Stream, 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. - Diagnostics and recovering parses. Every error has a
Y####code (code), a span, a path and, for common mistakes, a fix you can apply; see diagnostics onErrorandRender.Document::parse_recoveringnever fails, and you can still read and edit everything outside the broken regions. - Serialize and configure.
to_stringandto_writerwrite anySerialize, quoting every string that some reader would read as something else. Every entry point has a*_withtwin that takes an explicitSettings(there are no globals): the schema, YAML 1.1 behaviours, duplicate-key handling, output layout (Emit) and resource limits (Limits). - Migrate from
serde_yaml. The entry points keepserde_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 auseswap, andCompat::serde_yaml()reads and writes asserde_yamldoes where YAML 1.2 allows. The migration guide walks through it and lists every remaining difference with its reason. - Real-world programs.
examples/starts withedit_config.rs(the quick start as a program), then edits a Kubernetes manifest, a GitHub Actions workflow and a docker-compose file with merge keys, checking every comment’s owner before and after. Run one withcargo 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:Errorimplementsmiette::Diagnostic(code, severity, labels, help with the suggested fixes, and a documentation URL per code);DiagnosticReportdoes the same for aDiagnostic. Addsmiette7 without its default features (it bringscfg-ifandunicode-width).serde_json:TryFromconversions betweenValueandserde_json::Valuein both directions, andValue::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 onValue::to_json_with. Addsserde_jsonwith only itsstdfeature.__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.
§License
Licensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE or https://www.apache.org/licenses/LICENSE-2.0)
- MIT license (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 and SECURITY.md.
Modules§
- code
- Stable
Y####diagnostic codes and the single code registry.
Macros§
Structs§
- Code
- A stable diagnostic code, rendered as
Y####. - Comment
- One comment, as the comment API reads it.
- Comment
Explanation - Why a comment belongs where it does (
Document::explain_comment). - Comment
Id - A stable handle to a comment of a
Document. - Comments
- The comments a node owns (
Document::comments), each list in source order. - Compat
- Compatibility presets. Each returns a
Settingsthat can be refined further; presets never changeLimits. - Deserializer
- A YAML stream deserializer.
- Diagnostic
- A source diagnostic: a stable code, severity, message, labels, optional help, optional path to the affected node, and suggested fixes.
- Diagnostic
Report miette - A
Diagnosticas amiette::Diagnostic(featuremiette), optionally carrying the source text its spans point into. - Document
- One YAML document, lossless and editable.
- Document
Compound - A collection being serialized as a document of a
Serializer. - Document
Mut - Document
iof aStream, for editing in place: the guardStream::document_mutreturns. - Emit
- Output settings for the emitter and serializer.
- Entry
- A mapping entry for
NodeMut::entry: present or not. - Error
- An error from a oneyaml entry point: opaque and boxed, so
Result<T, Error>stays one pointer wide. - Label
- A span with a message, pointing at source text.
- Limits
- Resource limits: secondary safeguards against resource exhaustion.
- LineCol
- A 0-based line and column; the column is in the
ColumnUnitasked for. - Line
Index - Line starts of one source text, to turn UTF-8 byte offsets (the unit
of every
crate::Span) into line/column positions in UTF-8 bytes,chars, or UTF-16 code units. - Location
- A 1-based line and column (in
chars) plus the byte offset. - Mapping
- An insertion-ordered YAML mapping with any-
Valuekeys. - Mapping
Into Iter - An owning iterator over a mapping’s entries, in insertion order.
- Mapping
Iter - A borrowing iterator over a mapping’s entries, in insertion order.
- Mapping
Ref - A mapping node.
- NodeId
- A stable handle to a node of a
Document. Resolve it withDocument::node; a handle that names no node of that document yieldsEditError::StaleNode. - NodeMut
- A mutable handle to one node of a
Document: the edit methods, each returningResult. Obtained fromDocument::node_mut,Document::get_path_mut, and friends. - NodeRef
- A node of a
Document: a mapping, sequence, scalar, alias, or error region, with its span, properties, and handle. - Number
- A YAML number: an integer of any width, or a float.
- Parse
Path Error - Why a path string is not a
Path(seePath::parse). - Parsed
- The result of a recovering parse: always a value, with every
diagnostic, errors included. Check
Parsed::has_errors. - Path
- A path from the document root to a node.
- Reinsert
Error - A refused re-insertion of a
Removed(“nothing destroyed silently”): the reason, and the removed content with its comments, handed back so the caller can put it elsewhere (or keep it). The document is unchanged. - Removed
- A node removed from a
Document: its entry (or item) as written, with exactly the comments the rulebook gives it (“nothing destroyed silently”): its leading comment lines, its same-line comment, the comments inside it, and the footers of the collections inside it, with the blank lines they carry (rule 8; not those above a collection’s first entry, which are the collection’s). Every other comment stays with its owner. Re-insert it into the same document to move it (NodeMut::insert_removed), or drop it: - Replacement
- One text edit of a
Suggestion: replace the source bytes inspanwithtext. An empty span inserts; an emptytextdeletes. - Scalar
Ref - A scalar node.
- Sequence
- A YAML sequence of
Values. - Sequence
Ref - A sequence node.
- Serializer
- A serde
Serializerwriting YAML to anio::Write. - Settings
- All input and output settings.
- Span
- A UTF-8 byte range
start..endinto the source text. - Spanned
- A deserialized value with the UTF-8 byte span of the node it came from.
- Stream
- A YAML stream: every document of one input, sharing one syntax tree.
- Suggestion
- A suggested fix: a message and a set of non-overlapping source edits, applied together. Maps directly onto an LSP code action or a rustc-style suggestion.
- Tag
- A resolved tag, e.g.
!colorortag:example.com,2000:app/foo, after%TAGexpansion and%XXdecoding. - Tagged
Value - A value with a tag the schema does not resolve.
Enums§
- Applicability
- How safely a
Suggestioncan be applied without a human looking at it. - Code
Range - The area a
Codebelongs to. - Column
Unit - The unit a column is counted in.
- Comment
Owner - Who owns a comment. A mapping entry is named by its key’s
NodeId, a sequence item by its node’s; resolve them withDocument::node. Also whatDocument::attach_commenttakes to re-own a comment explicitly. - Comment
Rule - The numbered rule of
docs/comment-attachment.mdthat decided a comment’s owner. Numbers are stable within a major version; rule 8 (blank-line counts,CommentExplanation::blank_lines_before) decides no owner and has no variant. - Duplicate
Keys - What to do with a duplicate mapping key.
- Edit
Error - Why a
Documentnavigation or edit failed. Every variant says where it happened or what refused it; a failed edit changes nothing. - Encoding
- A character encoding of YAML input (§5.2).
- Error
Kind - What kind of problem an
Errorreports (from its code’s range). - Layout
- Overall output layout.
- Node
Kind - What a node is.
- Path
Segment - One step of a
Path. - Quote
Style - Which quote character the emitter prefers for strings that need quoting.
- Scalar
Style - How a scalar was written.
- Schema
- The schema that resolves plain scalars to types.
- Severity
- How serious a
Diagnosticis. - Value
- An owned YAML value.
Constants§
- NO_
COMMENTS - No comment lines: what the comment setters take to remove the lines
they set (an empty array’s item type cannot be inferred, so
[]alone does not compile there).
Traits§
- AsKey
- A mapping key for
Document::get,NodeRef::get,NodeMut::get_mut,NodeMut::insertand the other key lookups and inserts of the edit API. - Into
Path - A path for
Document::get_path,Document::set_pathand the other path methods: aPath(built withpath!), or a string in the syntax ofPath::parse, such as"spec.template.containers[0].image". - Render
- Rendering of a diagnostic-bearing value as a rustc-style snippet of its
source. Implemented for
Diagnosticandcrate::Error. - Value
Index - Something that can index a
Value: ausize(sequence position), astr/String(mapping key), or aValue(any mapping key).
Functions§
- from_
reader - Deserializes one document from a reader (any encoding, as
from_slice). - from_
reader_ with from_readerwith explicitSettings.- from_
slice - Deserializes one document from bytes in UTF-8, UTF-16, or UTF-32 (the encoding is detected from the byte order mark or the first bytes, §5.2). Error spans refer to the decoded UTF-8 text.
- from_
slice_ with from_slicewith explicitSettings.- from_
str - Deserializes one YAML document from a string, with default
Settings(YAML 1.2, Core schema,Limits::default()). - from_
str_ with from_strwith explicitSettings. Warnings are ignored: at most the first 16 are built, the rest counted; an error carries those (Error::warnings), then aY2010summary when there were more.from_str_with_diagnosticskeeps up toLimits::max_diagnostics.- from_
str_ with_ diagnostics from_str_with, also returning the warnings (settings conflicts,%YAMLdirectives, tolerated duplicate keys, …) in source order: the firstLimits::max_diagnostics, then acode::DIAGNOSTICS_SUPPRESSEDsummary counting the rest.- from_
value - Deserializes a
Tfrom aValue, with defaultLimits. - from_
value_ with from_valuewith explicitSettings. The limits apply (max_depth,max_typed_depth), and so doesSettings::resolve_tagged_variant_content: a typed enum readsTagged("!Square", String("2.0"))asSquare(2.0), asfrom_str_withreads!Square 2.0. Otherwise the value was resolved when it was built, and schema settings have nothing left to decide.- to_
string - Serializes
valueas a YAML document with the default settings: every-reader-safe quoting, 2-space indent, a trailing newline. - to_
string_ with to_stringwith explicit settings:Emitoutput options andLimits.- to_
value - Converts
valueto aValuewith the default settings:serde_yaml’sto_value, with its signature (valueby value;&xworks too, as&T: Serialize), so migrating is auseswap. The inverse isfrom_value. - to_
value_ with to_valuewith explicit settings:Limits::max_typed_depthandLimits::max_depthapply.- to_
writer - Serializes
valueas a YAML document intowriter, with the default settings, and flushes it. - to_
writer_ with to_writerwith explicit settings.