Contents
- Install — Cargo, MSRV
- Quick Start — parse and serialise in ten lines
- Why this approach? — design rationale
- Surface — what this crate exposes
- YAML 1.2 conformance — official-suite results
- Cargo features — feature-flag matrix
- Library Usage — common patterns
- Examples — runnable example index
- Benchmarks — performance evidence
- When not to use noyalib — limitations
- Migrating from another YAML crate — name-for-name mapping
- Documentation — extended reading
- License
Install
[]
= "0.0.54"
no_std (alloc-only) builds:
[]
= { = "0.0.54", = false }
Core data binding (from_str, to_string, Value, schemas) and
the streaming deserialiser run without the standard library.
from_reader, to_writer, the Spanned<T> deserialise helper
(uses thread-local storage), and the CST module require the
std feature, which is enabled by default.
Lean / FIPS / embedded profile — for users with strict
dependency budgets, default-features = false, features = ["std"] (or the equivalent features = ["minimal"] alias) drops
itoa, ryu, and serde_ignored. Numeric formatting falls back
to core::fmt (slower, output remains valid YAML); the
from_str_strict / from_slice_strict / from_reader_strict
typo-detection helpers are absent. Re-enable individually with
features = ["fast-int", "fast-float", "strict-deserialise"].
MSRV: Rust 1.86.0 (edition 2024), enforced by the msrv-core CI job.
Quick Start
use ;
let yaml = "host: api.example.com\nport: 8080\nfeatures: [auth, api]\n";
let cfg: Config = from_str.unwrap;
let out: String = to_string.unwrap;
let round: Config = from_str.unwrap;
assert_eq!;
Why this approach?
noyalib targets the niche serde_yaml / serde_yml / libyml
occupy — read YAML into typed Rust structs, write Rust structs
back as YAML — and is written from scratch against the YAML 1.2
spec. The implementation runs the official YAML test suite to
100% strict compliance — 406/406 attempted cases pass, 0
failures, 0 skips (verified by tests/yaml_compliance_report.rs).
It is not a fork of serde_yaml;
the parser, scanner, serialiser, and CST are independent code.
Two architectural choices motivate the rewrite:
-
Streaming-first deserialise. The default
from_str<T>path walks parser events directly into the typed target. Theserde_yaml-shaped pattern isparse → Value → T::deserialize(&Value), which allocates every key, scalar, and nested mapping into an intermediate AST that the typed target then throws away. noyalib bypasses that AST when the caller asked for a typedT. The dynamicValuetree is still available when callers want it, but it is no longer in the hot path. -
#![forbid(unsafe_code)]at the workspace root. No FFI to a C library, no raw-pointer dereferences, and nounsafeblocks in the parser, scanner, formatter, or CST. CI enforces the attribute on every push. Most popular Rust YAML crates wraplibyamlvia C-FFI; theunsafeblocks involved are usually well-vetted, but their existence makes a security-conscious downstream audit meaningfully harder.
A few features built on top of those choices: SIMD-accelerated
structural discovery (memchr SSE2/NEON for arity 1–3, SWAR
for 4–8, optional core::simd 32-byte structural-bitmask),
SWAR decimal parsing (~2× faster than <i64 as FromStr>), and
a side-table CST that reproduces the source byte-for-byte and
supports surgical edits.
The dependency tree is eight required crates: serde,
indexmap, rustc-hash, itoa, ryu, memchr, smallvec,
serde_ignored. No archived or unmaintained crates appear in
the graph — serde_yaml 0.9 (archived), libyaml (C-FFI),
and thiserror are all absent. cargo audit, cargo deny, and
cargo vet are CI gates on every push.
Surface
| Module | Use |
|---|---|
from_str, from_slice, from_reader, from_value |
Read YAML into T: Deserialize. Each has a _with_config variant. |
to_string, to_writer, to_value |
Write T: Serialize back as YAML. |
from_str_strict (+ _slice, _reader) |
Strict deserialise — error on any key the target type does not declare. Closes the silent-data-loss gap on config-key typos. |
Value |
Dynamic tree (7 variants: Null, Bool, Number, String, Sequence, Mapping, Tagged). Path queries via query("items[*].name"). |
Spanned<T> |
Wraps any T with (line, column, byte offset). Survives #[serde(flatten)]. |
cst::Document |
Lossless CST. doc.set("server.port", "9090") rewrites only the touched span; comments + indentation preserved. Full guarded mutator family — rename_key / rename_anchor / swap_items / move_item and the auto-formatting *_value inserts via cst::Emit, each rolled back on any typed-value mismatch. |
policy::{DenyAnchors, DenyTags, MaxScalarLength} |
Pluggable parser policies. Reject documents at parse time. |
schema_for, validate_against_schema, coerce_to_schema |
JSON Schema 2020-12 codegen, validation, and schema-driven autofix. |
parallel::parse, parse_with_config_in_pool |
Multi-doc parse across the global or a caller-owned Rayon pool. |
borrowed::from_str_borrowed |
Zero-copy AST. Scalars borrow from input bytes (~18 % faster). |
compat::serde_yaml |
Drop-in shim — use noyalib::compat::serde_yaml as serde_yaml. |
YAML 1.2 conformance
Validated against the
official YAML test suite
at 100% strict compliance — 406 / 406 attempted cases pass, 0
failures, 0 skips. The conformance report is tracked in
tests/yaml_compliance_report.rs and rebuilds on every CI run
via cargo test --test yaml_compliance_report.
The same suite also exercises:
- Anchors, aliases, and
<<:merge keys. - Flow vs block style mixing.
- Quoted, plain, literal (
|), and folded (>) scalar styles. - Multi-document streams (
---). - Every YAML 1.2 core schema tag (
!!int,!!float,!!bool,!!null,!!str).
Opt-in YAML 1.1 boolean parsing is available for backwards compatibility:
use ;
let cfg = new.legacy_booleans;
// "yes" / "no" / "on" / "off" parse as bool under legacy mode.
Cargo features
All optional integrations are off by default. Enable only what the application needs.
Structured diagnostics are part of the base API. Every Error exposes a
stable code, severity, help text, and zero or more byte-based source labels
through Error::diagnostic(). Renderer integrations remain opt-in.
| Feature | Pulls in | Adds |
|---|---|---|
std (default) |
— | from_reader, to_writer, Spanned<T>, CST module |
miette |
miette 7 |
Render the shared diagnostics in terminals |
schema |
schemars, serde_json |
JsonSchema derive + schema_for::<T>() |
validate-schema |
schema + jsonschema |
validate_against_schema, coerce_to_schema |
figment |
figment 0.10 |
noyalib::figment::Yaml provider |
garde |
garde 0.22 |
Validated<T> wrapper |
validator |
validator 0.19 |
ValidatedValidator<T> wrapper |
robotics |
— | Degrees / Radians / StrictFloat newtypes |
parallel |
rayon 1.10 |
parallel::parse<T> and parse_with_config_in_pool<T> |
recovery |
— | noyalib::recovery::parse_lenient — best-effort parsing for LSP / IDE half-typed documents |
sval |
sval 2 |
impl sval::Value for Value / Number / Mapping / MappingAny / TaggedValue, noyalib::sval_adapter::to_sval_writer |
tokio |
tokio, tokio-util, bytes |
Bounded async readers, backpressured AsyncYamlStream, and the lower-level YamlDecoder codec |
simd |
— | noyalib::simd::* primitives + parser hot path |
nightly-simd |
simd (nightly) |
core::simd-backed 32-byte structural-bitmask scanner |
compat-serde-yaml |
— | noyalib::compat::serde_yaml shim for migration |
Library Usage
Read into typed structs
use from_str;
let s: Server = from_str?;
# Ok::
Strict deserialise (reject unknown keys)
let yaml = "port: 8080\nhost: api\nporrt: 9090\n"; // typo
// Lenient: silently drops the typo.
let _ : Cfg = from_str?;
// Strict: typed error pointing at `porrt`.
let err = .unwrap_err;
assert!;
# Ok::
Lossless CST edit
use parse_document;
let mut doc = parse_document?;
doc.set?;
assert!; // comment preserved
# Ok::
The full editor also covers rename_key / rename_anchor,
swap_items / move_item, and the auto-formatting
insert_entry_value / push_back_value / insert_after_value inserts
(typed values are quoted so they re-parse as data, not YAML syntax —
guarded by a typed-value oracle that rolls back on any mismatch).
Untrusted input is bounded by eight resource budgets, including
max_nodes (AST node-count floods).
Source spans
use ;
let cfg: Cfg = from_str?;
assert_eq!;
assert_eq!;
# Ok::
Examples
76 runnable examples under
crates/noyalib/examples/:
Categories (full list in
examples/): Core (hello, std, variants,
deep, dynamic, modify, tags); Spec (alias, smart,
overlay, inherit, stream, types, binary); Logic &
Security (strict, secure, schema, env); DX (errors,
trace, source, style); Advanced (emit, rename,
flatten, bridge, pipes, global); Future-Proof
(portable, mask, patch, suggest, schema_ext); Deep
Rust (untagged, borrow, transcode, comments); Platform
(diagnostic, nostd, preserve); Ecosystem
(include, figment, validation_garde,
validation_validator, diagnostic_path,
robotics_polymorphism).
Benchmarks
Reproducible on Apple M-series + ubuntu-latest. Published
throughput tables sit in the
Benchmarks section of the workspace README:
noyalib is faster than every other pure-Rust YAML library
on every deserialize fixture measured. Speedup ranges:
1.69×–2.00× vs serde-saphyr,
1.42×–1.84× vs serde_yaml_ng, 1.38×–1.74× vs
yaml-spanned, 1.11×–1.36× vs yaml-rust2. Serialize is
3.00×–4.34× ahead of serde_yaml_ng. The structural-bitmask
scanner runs 4.2× (stable) / 9.2× (nightly-simd) over the memchr
loop on 1 MiB documents; the SWAR decimal parser runs 2.17–2.52×
faster than the stdlib from_str.
When not to use noyalib
- You need to round-trip comments through the data-binding
API. The YAML data model excludes comments by spec. The
lossless CST (
noyalib::cst::Document) preserves them byte-for-byte for the tooling path, butfrom_str::<T>→to_string(&T)does not. No Rust YAML library currently round-trips comments through a typed deserialise / serialise pair. - You need a different YAML 1.1 resolution rule than the three
the spec actually disagrees with 1.2 on. noyalib defaults to
YAML 1.2 strict semantics; opt in via
ParserConfig::version(YamlVersion::V1_1)to flip the three resolver-table differences (yes/no/on/offbooleans, bare-0octal0644, sexagesimal60:00) on as a bundle. Other 1.1-isms (mandatory!!tag prefix, broader timestamp parsing) are not currently version-gated; both 1.1 and 1.2 mode accept the relaxed forms.
Migrating from another YAML crate
The headline diff for serde_yaml 0.9 plus a name-for-name
mapping table sits in the
workspace README.
The deeper guide also covers serde_yml, yaml_serde,
serde-yaml-ng, serde-norway, serde-yaml-bw,
serde-saphyr, and yaml-spanned — per-crate function tables,
behavioural-difference notes, the drop-in shim, and a
migration checklist:
docs/MIGRATION-FROM-SERDE-YAML.md.
Documentation
- API reference: https://docs.rs/noyalib
- Engineering policies (MSRV, SemVer, security, performance, concurrency, platform support, feature flags):
docs/POLICIES.md - Migration guides (
serde_yamland 7 other YAML crates):docs/MIGRATION.md - Security policy:
SECURITY.md - Internals (module map, hot paths):
docs/internals.md - Error reference:
docs/errors.md - User guide:
docs/USER-GUIDE.md - Architecture overview:
docs/ARCHITECTURE.md - Workspace README: https://github.com/sebastienrousseau/noyalib#readme
License
Dual-licensed under Apache 2.0 or MIT, at your option.