The noyalib-serde-yaml Manual
The drop-in serde_yaml replacement: rename the package, change zero source lines. noyalib-serde-yaml is one of the six crates of the noyalib family and
releases in lockstep with the core library at the identical =0.0.X.
This manual is the rendered form of the Markdown in the repository’s
docs/
directory; the repository copy is canonical. The family manual for the
core library, with the user guide, migrations, and policies, is at
https://sebastienrousseau.github.io/noyalib/manual/.
Pages
| Page |
|---|
| The serde_yaml behavioural contract |
| Migrating from serde_yaml — the one-line version |
| Architecture |
Install, quick start, and the full surface are in the README. The CHANGELOG is the complete release record.
The serde_yaml behavioural contract
“Drop-in replacement” is a behavioural claim, so it is held by a test suite, not a README sentence.
Provenance
The corpus began life as an evaluation harness built by a
prospective adopter to decide whether noyalib could replace
serde_yaml in their build
(zudo-front-builder#2787).
noyalib 0.0.28 diverged on 11 of its 18 cases and was rejected —
the most rigorous public specification of what serde_yaml
compatibility actually means. noyalib v0.0.29 vendors that corpus as
its own contract suite
(crates/noyalib/tests/serde_yaml_contract.rs), with every
expectation captured live from serde_yaml 0.9.34+deprecated:
the JSON value produced, the error Display string, and the
location() line/column/index. All 18 pass.
Enforcement
- The suite runs in the core’s
cargo test --all-features— every PR, every platform in the test matrix. - A differential fuzzer (
fuzz_serde_yaml_compat) runs the shim against the real archivedserde_yamlon arbitrary inputs, hunting for divergences nobody has written down yet; confirmed legitimate differences graduate into its documented allowlist, everything else is a bug. no_stdcompatibility of the shim is CI-enforced on wasm32 and three bare-metal targets.
Migrating from serde_yaml — the one-line version
# Cargo.toml — the whole migration:
serde_yaml = { package = "noyalib-serde-yaml", version = "=0.0.58" }
Every use serde_yaml::… in your codebase keeps compiling and
behaving. That is the entire recipe; the rest of this page is what
“behaving” means and where the edges are.
What is guaranteed
The shim parses under the core’s ParserConfig::serde_yaml_compat()
profile and renders errors upstream-style. The 18-case contract
suite in the core repository
(crates/noyalib/tests/serde_yaml_contract.rs) pins — with
expectations captured live from serde_yaml 0.9.34+deprecated —
values, error Display text, and location() line/column/index for:
anchors and aliases, merge keys, scalar and composite keys, YAML 1.1
boolean spellings, octal/binary/sexagesimal numbers, null and date
scalars, Unicode/CRLF/emoji, malformed-input locations, built-in and
custom tags, duplicate keys, non-finite and overflowing numbers,
integer boundaries, and alias resource limits.
The edges
- Custom tags under
deserialize_anyrefuse with upstream’s message, but anchor the location at the value rather than the tag. - Error wording beyond the pinned classes may use noyalib’s own phrasing — upstream’s message set is unbounded; the classes real code pins (per the contract corpus) are matched.
- Spec-vs-libyaml acceptance gaps exist in both directions
(libyaml rejects spec-legal anchor names like
&\L; empty implicit keys; etc.). noyalib follows the spec — it passes 406/406 of the official YAML test suite.
When you outgrow drop-in
The same engine offers YAML 1.2 strict resolution, byte-exact
lossless editing (noyalib::cst), source spans, streaming
deserialisation, and schema validation. Depend on noyalib directly
(optionally with features = ["compat-serde-yaml"] to keep the shim
during a gradual migration), and see the core’s
docs/MIGRATION-FROM-SERDE-YAML.md
for the full function-by-function mapping.
serde_yaml migration comparison
| Option | Source changes | Maintained engine | Legacy behaviour retained |
|---|---|---|---|
noyalib-serde-yaml | No | Yes | Yes, by documented contract |
Direct noyalib | Yes | Yes | Opt-in compatibility profile |
Archived serde_yaml | No | No | Yes |
Use this facade when a reversible package rename and known legacy behaviour are
more important than adopting the native noyalib API immediately. Prefer direct
noyalib for greenfield code. The detailed behavioural boundary and captured
evidence are in the contract.
Benchmarks
The shim_overhead Criterion harness compares the compatibility profile with a
direct noyalib path using the same documents.
cargo bench --bench shim_overhead
Record the CPU, operating system, Rust version, commit, input corpus, and command with results. The compatibility profile may materialize data required by legacy merge-key semantics, so compare equivalent behaviour. CI builds benchmark targets but does not gate releases on shared-runner timing.
Architecture
noyalib-serde-yaml is deliberately the smallest crate in the family.
Its whole source is one line:
#![allow(unused)]
fn main() {
pub use noyalib::compat::serde_yaml::*;
}
Everything else is contract.
What the re-export gives you
noyalib::compat::serde_yaml is the core library’s behavioural
serde_yaml 0.9 shim: the same function names, types, and error
phrasing, pinned by an 18-case contract suite captured live from
serde_yaml 0.9.34 and kept in the core repository
(tests/serde_yaml_contract.rs). Publishing that module under a
package name lets Cargo’s package = rename substitute it for
serde_yaml without a single source change downstream.
Why a separate crate
A dependency rename can only target a crate on the registry with the right name. The core cannot be renamed, so a one-line crate is the smallest artefact that makes the migration a Cargo.toml edit. Keeping it empty of logic means there is nothing here to drift from the core.
Features
The crate enables exactly std and compat-serde-yaml on the core,
with default features off, so it pulls nothing the shim does not need.
Testing
tests/drop_in.rs exercises the surface as a renamed dependency would
see it. docs/CONTRACT.md records the behavioural guarantees. Two
libFuzzer targets in fuzz/ feed arbitrary input through the shim’s
from_str and a to_string round trip, and CI replays the seed corpus
on every push. The README’s Rust block is compiled in CI against this
crate under the rename.
Lockstep
The crate pins noyalib at the identical =0.0.X and releases with
it (core ADR-0005). The exact pin is the compatibility contract: the
behaviour a user gets is the behaviour that version of the core tested.
Engineering policies
Version and dependency policy
noyalib-serde-yaml releases in strict lockstep with noyalib. Version 0.0.58
pins the core at exactly =0.0.58. Release work uses feat/v0.0.58; each
subsequent iteration increments exactly 0.0.1.
Minimum Rust version
The minimum supported Rust version is 1.89.0, declared in the manifest and verified by CI. A floor increase is a breaking-axis change and requires a changelog entry.
Compatibility
The documented serde_yaml behaviour, error display and location shapes,
package-rename path, and exported names are public. Production manifests must
not contain a local path override. During 0.0.x, the patch component is the
breaking axis.
Family-wide policies live in the core
POLICIES.md.
Repository standard compliance grade
Assessment date: 2026-09-20. Source rubric:
/Users/seb/Code/REPO-STANDARD.md. This is a conservative static assessment of
checked-in evidence, not a substitute for successful CI runs.
Result
14/24 signals (58%). README template: pass. Strict candidate tier: L1.
| Category | Signals | Summary |
|---|---|---|
| Identity and README | 3/3 | Canonical structure and compatibility policy present |
| Documentation | 2/3 | Manual, architecture, migration, and contract docs present |
| Build and install UX | 1/3 | Native Cargo build present; CLI-generation requirements are not applicable evidence |
| Releases and binaries | 2/3 | Registry automation present; complete L3 evidence remains open |
| Packaging | 1/3 | Registry distribution only; wider tracking and reproducibility are open |
| CI quality gates | 2/3 | Matrix and coverage signals present |
| Supply chain | 2/3 | Foundation controls present; advisory-audit detection remains open |
| Community | 1/3 | Foundation governance present; L2 template and docs-lint signals are open |
The cumulative tier remains L1. Priority work is distribution tracking, reproducible-build evidence, explicit advisory-audit CI, and audit-visible community templates and markdown lint enforcement.
Detailed README reference
noyalib-serde-yaml
Drop-in serde_yaml replacement backed by
noyalib — rename the package in Cargo.toml, change zero source
lines, keep the behaviour your code depends on.
Contents
- Install — the one-line migration
- Requirements — toolchain floor, platforms, the core pin
- Quick Start — pre-migration code, unchanged
- Why this approach? — design rationale
- Behavioural parity — what “drop-in” guarantees
- Examples — runnable demonstrations
- Benchmarks — the cost of compatibility
- Repository layout
- Developing against an unpublished core
- When not to use noyalib-serde-yaml
- Documentation
- License
Install
# Cargo.toml — the whole migration:
serde_yaml = { package = "noyalib-serde-yaml", version = "=0.0.58" }
Every use serde_yaml::… in your codebase keeps compiling — Cargo’s
package rename substitutes this crate under the old name, so the
diff is one manifest line.
MSRV: Rust 1.89.0, the floor shared across the noyalib family and the
maintainer’s other Rust crates (noyalib ADR-0014). Releases
ship in strict lockstep with the core at the identical =0.0.X
(ADR-0005); the exact pin is the compatibility contract.
Requirements
- Rust 1.89.0 or newer to build from source:
rust-versionin the manifest, enforced by themsrv-coreCI job on every push. - Any tier-1 platform. CI runs the tests on Linux, macOS, and Windows with the stable, beta, and nightly toolchains; stable is the gate, beta and nightly are early warning.
- The matching core. This crate pins
noyalibat the identical=0.0.Xand releases in lockstep with it; Cargo resolves that pin for you. serde1, as withserde_yaml; nothing else changes in your manifest beyond the package rename.
Quick Start
This is serde_yaml 0.9 code, byte for byte — only the Cargo.toml
line above has changed:
#![allow(unused)]
fn main() {
use serde_yaml::{from_str, to_string};
#[derive(serde::Serialize, serde::Deserialize)]
struct Config { name: String, port: u16 }
let cfg: Config = from_str("name: gateway\nport: 8443\n")?;
let out = to_string(&cfg)?;
let err = from_str::<serde_yaml::Value>("a: [unclosed").unwrap_err();
let loc = err.location().unwrap(); // 1-based line/column, 0-based index
}
Why this approach?
serde_yaml 0.9 was archived in March 2024; advisories and spec
corrections no longer flow into it. Every successor asks you to edit
call sites — and then behaves differently anyway. This crate takes
the opposite contract on both counts:
- Zero source changes. The migration is a package rename in one manifest line, reversible with the same line.
- Behaviour, pinned. Since noyalib v0.0.29 the shim this crate
re-exports is behavioural: it parses under the core’s
serde_yaml_compat()profile and renders errors upstream-style, held by an 18-case contract suite whose expectations were captured live fromserde_yaml 0.9.34+deprecated— values, errorDisplaytext, andlocation()pins. - No dead branch. The archived crate and its advisory chain are
never dependencies — every type is noyalib-native under the
serde_yamlnames, pure Rust,#![forbid(unsafe_code)].
Behavioural parity
serde_yaml 0.9 behaviour | this crate |
|---|---|
<<: stays a literal key (alias value resolved) | ✅ |
0123 is a string, 0b11 is 3 | ✅ |
1e999 stays the string "1e999" | ✅ |
u64::MAX keeps full precision; one past it errors JSON number out of range | ✅ |
[a, b]: v errors invalid type: sequence, expected a string key at 1:1:0 | ✅ |
alias bombs error repetition limit exceeded (jumps ≤ events × 100) | ✅ |
| libyaml error phrasing and end-of-input location convention | ✅ pinned classes |
Error::location() → 1-based line/column, 0-based index | ✅ |
One documented partial: a custom tag refused under deserialize_any
anchors its location at the value where upstream anchors at the tag —
message and refusal semantics are exact. Full provenance and
enforcement: docs/CONTRACT.md.
Examples
cargo run --example drop_in # the rename migration end to end
cargo run --example behavioural_parity # every headline quirk, live
Benchmarks
cargo bench # shim path vs direct noyalib on the same documents
The shim’s profile deliberately bypasses noyalib’s streaming fast path (merge keys must materialise), so the number worth watching is how small that gap is on your documents.
Repository layout
| Path | What lives there |
|---|---|
src/lib.rs | The whole crate: a re-export of noyalib::compat::serde_yaml |
tests/drop_in.rs | Pre-migration serde_yaml code, compiled and run verbatim |
examples/ | The migration and the parity quirks, runnable |
benches/shim_overhead.rs | Shim path vs direct noyalib |
docs/MIGRATION.md | The one-line migration, its guarantees, and its edges |
docs/CONTRACT.md | Where the 18-case behavioural contract comes from |
Developing against an unpublished core
Between lockstep releases the =0.0.X pin can point at a core
version crates.io does not have yet. During that window a
[patch.crates-io] section resolving it against the core’s release
branch is committed so CI and local builds work; the patch is
removed at release time (the release validation refuses to tag
while it is present). To develop against your local checkout
instead:
cargo test --config 'patch.crates-io.noyalib.path="../noyalib/crates/noyalib"'
When not to use noyalib-serde-yaml
- Greenfield code. Depend on
noyalibdirectly — YAML 1.2 strict resolution, byte-exact lossless editing (noyalib::cst), source spans, streaming deserialisation, and schema validation, without the legacy quirks this crate exists to reproduce. - You want the quirks gone. The same engine minus the profile is
one
use noyalib::…away; migrate gradually with the core’scompat-serde-yamlfeature.
Documentation
The four entry points, identical across every repo in the family:
-
User Manual — this crate’s rendered book: its guides, architecture, and release notes; the family manual for the core library is at https://sebastienrousseau.github.io/noyalib/manual/
-
API reference — rustdoc on docs.rs
-
Developer docs — this repo’s dev entry point, pointing at the family guide
-
Ecosystem map — the six crates, the lockstep model, the scorecard
-
Migration guide —
docs/MIGRATION.md, and the core repository’sMIGRATION-FROM-SERDE-YAML.mdfor the function-by-function mapping. -
Behavioural contract —
docs/CONTRACT.md. -
API reference — docs.rs/noyalib-serde-yaml.
-
Engineering policies — the core repository’s
POLICIES.md.
Conformance
Every push runs the official yaml-test-suite
through this shim’s from_str, from the same vendored suite and the same core
commit as the noyalib core: 367 of 367 cases inside the serde_yaml
contract (24 multi-document cases and 15 non-scalar-key cases are refused the
way the original refused them). A configuration that uses most of YAML at
once (tests/fixtures/ultra-complex/) parses to exactly its expected JSON.
Details and the family table:
noyalib.com/conformance.
License
Licensed under either of Apache License, Version 2.0 or MIT license at your option.