Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Migrating from serde_yaml

serde_yaml is archived. For config-shaped Serde code, saneyaml is close to a drop-in: the read API, Value, and the with::singleton_map helpers keep the same spelling — now with YAML 1.2 scalar resolution and richer diagnostics.

This page is the call-site cookbook. The exhaustive support matrix, divergences, and threat model live in COMPATIBILITY.md; the scalar-typing differences are in Schema modes.

Scope. saneyaml is an adoption candidate for config-shaped Serde reads plus structural writes. It is not a blanket drop-in for every YAML document, every emitter byte, or full YAML 1.1 / libyaml behavior.

Two ways to switch

Keep serde_yaml::… spellings — alias the package in Cargo, change nothing in source:

[dependencies]
serde_yaml = { package = "saneyaml", version = "0.3.0" }

serde_yaml::from_str, serde_yaml::Value, serde_yaml::with::singleton_map, and friends keep compiling against this crate.

Or import directly and rewrite the prefix:

[dependencies]
saneyaml = "0.3.0"
#![allow(unused)]
fn main() {
// mechanical rename
let cfg: saneyaml::Value = saneyaml::from_str(input)?;

// …or alias one file at a time
use saneyaml as serde_yaml;
let cfg: serde_yaml::Value = serde_yaml::from_str(input)?;
}

The shipped examples/serde_yaml_migration.rs compiles the full alias surface end to end.

Cookbook

Each recipe shows the call site. Under a Cargo/source alias, keep the serde_yaml:: spelling; with a direct import, swap the prefix to saneyaml::.

Typed reads — unchanged:

#![allow(unused)]
fn main() {
let config: Config = saneyaml::from_str(input)?;
let config: Config = saneyaml::from_slice(bytes)?;
let config: Config = saneyaml::from_reader(reader)?;
}

Value indexing and patching — unchanged:

#![allow(unused)]
fn main() {
let mut value: saneyaml::Value = saneyaml::from_str(input)?;
value["services"]["api"]["image"] = saneyaml::Value::from("nginx:latest");
let ports = value["services"]["api"]["ports"].as_sequence();
}

Tagged enums / singleton maps — same helpers:

#![allow(unused)]
fn main() {
#[derive(serde::Serialize, serde::Deserialize)]
struct Job {
    #[serde(with = "saneyaml::with::singleton_map")]
    action: Action,
}
}

Use singleton_map_recursive for nested enum payloads.

Multi-document streams — iterate, or collect:

#![allow(unused)]
fn main() {
let docs = saneyaml::Deserializer::from_str(stream)
    .map(Config::deserialize)
    .collect::<Result<Vec<_>, _>>()?;

let docs: Vec<Config> = saneyaml::from_documents_str(stream)?; // additive convenience
}

Structural writes — unchanged:

#![allow(unused)]
fn main() {
let text = saneyaml::to_string(&config)?;
saneyaml::to_writer(&mut writer, &config)?;
}

Errorsline() / column() still work; span(), category(), path(), and render_source() are additive (Diagnostics):

#![allow(unused)]
fn main() {
let err = saneyaml::from_str::<Config>("name: [").unwrap_err();
if let Some(loc) = err.location() {
    eprintln!("{}:{}", loc.line(), loc.column());
}
}

What behaves differently

Five things a migrator should know — most code never touches them:

ChangeWhat it means for you
YAML 1.2 by defaultno / on / NO stay strings, not booleans. Opt into the old behavior per call with LoadOptions::legacy_serde_yaml(). See Schema modes.
Merge keys expand by defaultLoaded Node/Value give you the merged result. serde_yaml::Value kept the literal << until apply_merge(). To see raw <<, use events or the lossless graph.
Value is spanlessIt won’t coerce a number/bool into a String target, and it doesn’t carry comments, anchors, or graph identity. Read with from_str/from_node when source text matters; use the lossless graph for formatting.
Structural writerto_string emits clean deterministic YAML, not byte-identical serde_yaml. Pass EmitOptions::byte_compatible() for the supported byte corpus.
Resource limits on by defaultUntrusted input is bounded (64 MiB, depth, scalar, collection, alias). Tune or opt out via LoadOptions. See Untrusted input.

Support matrix

All of the following resolve under both rename paths and are covered by the swap harness and downstream smokes:

serde_yaml surfaceStatus
from_str / from_slice / from_readerCovered for typed config reads and Value
Deserializer::{from_str, from_slice, from_reader}Covered, incl. multi-document iteration
Value / Mapping / NumberCovered: reads, mutation, indexing, helpers, traits
value::{to_value, Serializer}Covered for config-shaped serialization
to_string / to_writer / SerializerStructural output covered; byte_compatible() matches bytes on the supported corpus
with::singleton_map / singleton_map_recursiveCovered for read and write
Error / Result / LocationCovered; richer diagnostics are additive

The indexing traits (Index, mapping::Index) are sealed, as they were upstream — use the built-in string / usize / Value lookups.

Proof

The migration claims are executable, not aspirational:

  • tests/serde_yaml_swap_harness.rs — the same call sites run against serde_yaml 0.9.34 and against this crate under the serde_yaml dependency name.
  • tests/downstream_migration_harness.rs and tests/external_downstream_migration.rs — pinned real-world configs and reduced fixtures from real serde_yaml users (Pingora, rust-i18n, cfn-guard, navi, Stackable).
  • scripts/downstream-build-trials.sh — packages this crate and builds those downstreams with their serde_yaml dependency rewritten to it.
cargo test --test serde_yaml_swap_harness --test downstream_migration_harness
cargo test --test external_downstream_migration
scripts/downstream-build-trials.sh smoke-only

Real-world gates currently cover 33 files / 39 documents across GitHub Actions, Docker Compose, Kubernetes, Helm, OpenAPI, Wrangler, Ansible, CloudFormation/SAM, Symfony, GitLab CI, CircleCI, and Azure Pipelines. They prove the selected corpus — not a substitute for testing your own YAML.

Migration impact ledger

AreaMigration impact
Default merge expansionLoaded Node/Value and Serde reads expand untagged and explicit merge-tag << by default. Code that inspected merge syntax should switch to parse_events or LosslessStream. Explicit !!str << and custom-tagged << stay literal.
YAML 1.1 compatibilityLegacy scalar/merge behavior is opt-in via schema modes; default entrypoints stay YAML 1.2-oriented, so corpora that need 1.1 typing need opt-in tests.
Alias graph identitySemantic trees clone acyclic aliases and reject recursion; graph-sensitive callers should use LosslessStream.
Lossless formattingComments, anchors, directives, and source style are preserved only by LosslessStream / ConfigEditor, not the semantic Value tree.
Parser acceptance differencesSome YAML 1.2 inputs libyaml rejects are accepted, and some malformed libyaml-tolerated inputs are rejected. Per-case detail lives in the divergence records.
Package statusCargo.toml declares saneyaml 0.3.0 under the MIT license.

Known follow-up

  • Keep the named external crate build trials current before broadening ecosystem replacement claims.
  • Keep divergence records and migration-impact wording current as behavior changes.
  • Treat full YAML compatibility and arbitrary source-preserving emission as future work until they are fixture-backed.