Skip to content

Build1 publisher3 min readPublished

Four YAML parsers, two specs: the failure mode is both of them being right

YAML 1.2 killed the Norway problem in 2009. A dev.to post makes the operator's point: if half your parsers never moved, the file is read two correct and incompatible ways, silently.

The Engineer · Build desk

Drafted by a language model from the sources cited here and checked against its claim ledger before publication. How we use AISend a correction

Illustration accompanying Four YAML parsers, two specs: the failure mode is both of them being right
Generated illustration

What happened

  • YAML 1.2 fixed the Norway problem (country: NO resolving to false) in 2009, but a large share of the parsers in a typical pipeline never moved to 1.2.
  • The interesting failure is not one parser being wrong but two parsers in the same pipeline being right in different ways on the same file.
  • A fairly ordinary Python-and-Kubernetes shop is running at least four YAML parsers; most teams have never counted.
  • js-yaml v3 implements YAML 1.1 and v4 implements 1.2, so a repo old enough to still be on v3 alongside a newer package on v4 has the spec split inside a single node_modules.
  • Practical divergences: country: NO -> 1.1 false, 1.2 "NO"; enabled: off -> 1.1 false, 1.2 "off"; mode: 0644 -> 1.1 420 (octal), 1.2 644 (decimal); timeout: 12:30 -> 1.1 750 (base 60), 1.2 "12:30"; scale: 1e5 -> 1.1 "1e5" (string), 1.2 100000; version: 1.10 -> 1.1 (float) in both.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

YAML 1.2 removed the implicit booleans that turn `country: NO` into `false`, and it did that in 2009 [1]. A post on dev.to argues the part that actually costs an afternoon is downstream of that fix: a large share of parsers in a working pipeline never moved to 1.2, so the failure is not one parser being wrong, it is two parsers in the same pipeline being right in different ways on the same file [1][2]. The count is the useful bit. According to the post, a fairly ordinary Python-and-Kubernetes shop runs at least four YAML parsers [3]. You do not need four repos to get the split, either: js-yaml v3 is 1.1 and v4 is 1.2, so a repo old enough to still carry v3 alongside a newer package on v4 has both specs inside a single `node_modules` [4]. Six lines cover most of it in practice: `country: NO` (1.1 `false`, 1.2 `"NO"`), `enabled: off` (`false` / `"off"`), `mode: 0644` (`420` as octal / `644` as decimal), `timeout: 12:30` (`750` base 60 / `"12:30"`), `scale: 1e5` (string / `100000`), and `version: 1.10`, which is the float `1.1` under both [5]. Five of the six diverge on spec version [1]. Three of them are silent data corruption with no error anywhere [6]. `mode: 0644` is the one that looks safe. A 1.1 parser reads octal and hands you `420`, which is the mode you meant; a 1.2 parser reads decimal `644`, which is `1204` in octal, which is nothing you wanted [5][7]. `12:30` becoming `750` is the sexagesimal type, a 1.1 feature that existed so you could write `1:30:00`, removed in 1.2; any port pair or `HH:MM` in a config is exposed [8]. Two more are not version differences at all. `1.10` is a float everywhere and floats have no trailing zeros, so a version pin silently becomes `1.1`, with no spec version in which that behaves [9]. Duplicate keys are an error under 1.2 and silent last-one-wins under PyYAML, so the same key at line 40 and line 380 of a 400-line values file goes unreported [10]. The expensive shape is a chain, not a file. The post's example: `feature_legacy_import: no` in `group_vars/prod.yml`, PyYAML resolves `False`, Jinja renders `False` into a ConfigMap as the string `"False"`, and the Go service calls `strconv.ParseBool`, which succeeds. Someone later "fixes" the vars file by quoting it to `"no"`, `ParseBool("no")` returns an error, and the code treats the error as `false` with a logged warning nobody reads. The flag is off both times, for two different reasons, and only one of them is the one that was configured [11]. Do not expect the linter to hold this. `yamllint` has a `truthy` rule that flags bare `yes`/`no`/`on`/`off`, and it is free to turn on [12], but it does not check octal ambiguity, sexagesimals, exponent notation, version truncation, or what two specific parsers each make of the file, because it reads YAML as text rather than values [13]. There is also a layer below the spec: the 1.1 type repository lists bare `y` and `n` as booleans and PyYAML's resolver deliberately does not, so "it works in my parser" is a claim about one library's resolver, not about YAML 1.1 [14]. The post's remedies, in its order of value: single-quote the ambiguous strings only (anything matching `y|n|yes|no|on|off|true|false` in any casing, leading zeros, colons, number-shaped non-numbers), because single quotes pin a string under both specs [15]; schema-validate the parsed object rather than the text, since a JSON Schema catches `420` where you wanted `"0644"` and a text linter cannot [16]; `yaml.safe_load` plus explicit coercion at the boundary instead of trusting the resolver [17]; and pin the parser version in the lockfile knowing which spec it implements, since js-yaml v3 to v4 is a behaviour change [18].

Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories