0002. YAML 1.2 strict semantics by default; 1.1 opt-in
- Status: accepted
- Date: 2026-05-06
- Authors: Sebastien Rousseau
Context
YAML 1.1 (2005) and YAML 1.2 (2009) disagree on plain-scalar resolution in three places:
| Form | YAML 1.1 | YAML 1.2 (core schema) |
|---|---|---|
yes / no / on / off | bool | string |
0644 (bare zero prefix) | int 420 (octal) | int 644 (decimal) |
60:00 (colon-separated) | int 3600 (sexagesimal) | string |
The 1.1 lexicon is responsible for the Norway problem: the
country code for Norway is NO, so YAML 1.1 silently coerces
country: NO to country: false. This has been a real-world
data-loss bug in production for over a decade.
YAML 1.2 narrowed the boolean lexicon specifically to fix this. But existing 1.1 documents — Ruby YAML configs, pyyaml output, crontab-style time-of-day notation — exist in the wild and cannot be unilaterally re-parsed under 1.2.
The library has to default to one of these. Defaulting to 1.1 preserves quiet bug-compatibility with old configs but ships a known correctness hazard. Defaulting to 1.2 surfaces the correctness hazard at parse time but breaks documents that genuinely intended 1.1 semantics.
Decision
noyalib defaults to YAML 1.2 strict semantics. Plain scalars follow the core schema:
yes/no/on/off→ string0644→ integer 644 (decimal)60:00→ string
YAML 1.1 semantics are available as an opt-in via
ParserConfig::version(YamlVersion::V1_1), which flips three
fine-grained flags as a bundle:
#![allow(unused)]
fn main() {
cfg.legacy_booleans = true; // yes / no / on / off
cfg.legacy_octal_numbers = true; // 0644 → octal
cfg.legacy_sexagesimal = true; // 60:00 → 3600
}
The fine-grained flags remain available so callers can mix and
match — e.g. “1.1 booleans but reject octal 0644”.
%YAML 1.1 directives in the document body are recorded but do
not auto-enable 1.1 mode. Honouring them silently would be the
exact silent-coercion failure mode 1.2 was created to fix; users
who want directive-driven version selection can layer it on top
of ParserConfig.
Consequences
Positive:
- The Norway problem cannot bite a noyalib user by default.
country: NOis a string, and stays a string. - The 1.2 spec is the active spec — defaulting to it aligns noyalib with the YAML committee’s stated direction.
- Migration from 1.1-era libraries is a single
version()call, not a full rewrite. The opt-in is loud but cheap.
Negative:
- Documents that genuinely use 1.1 sexagesimal (
crontab: 1:30) silently parse as strings under default settings. Users who expect 1.1 must explicitly opt in. This is the trade-off the decision makes — surfacing the discrepancy beats coercing it. - Double documentation: the README has to explain both modes, and the per-feature flags exist independently of the version preset, so the surface is slightly larger.
Neutral:
- noyalib accepts certain 1.1-tolerant forms regardless of version (e.g. tab-relaxed indentation in flow contexts) where doing so does not introduce silent data loss. The boundary is “would this cause a Norway-style coercion?” — if no, accept; if yes, gate behind the version flag.
Alternatives considered
Default to 1.1 for compatibility
Match serde_yaml 0.9’s behaviour and accept the Norway problem
as a documented quirk. Rejected — serde_yaml’s archival in 2024
was partly driven by the inability to fix this without breaking
its existing user base. noyalib gets the freedom to fix it;
declining would be an own goal.
Auto-detect from %YAML 1.1 directive
Read the first directive in the stream and auto-enable 1.1 mode
when present. Rejected because documents commonly carry stale
directives — a %YAML 1.1 from 2008 in a file that now expects
1.2 behaviour is worse than the manual opt-in. The directive is
recorded for inspection; auto-application would be silent
coercion in disguise.
Per-call version override only (no default)
Force every caller to specify a version explicitly. Rejected because the 99% case is “I have a YAML file, parse it” — adding mandatory version selection is friction that yields no correctness benefit for the ~95% of users on 1.2-compatible documents.
Strict 1.2 only, no 1.1 mode at all
Drop legacy_* flags entirely; force all users to migrate.
Rejected because YAML 1.1 documents exist and noyalib’s
selling point is being able to parse them losslessly. Refusing
to handle them would push users back to serde_yaml’s archived
fork.
References
- YAML 1.2 spec, §10.2: https://yaml.org/spec/1.2.2/#102-tags
- Norway problem write-up: https://hitchdev.com/strictyaml/why/implicit-typing-removed/
- Implementation:
crates/noyalib/src/de.rs—YamlVersion,ParserConfig::version - Test coverage:
crates/noyalib/tests/yaml_version.rs(11 tests) - Commit that introduced
version():1cffa2c