The Complete Guide to YAML Pitfalls: From the Norway Problem to Unsafe Loaders
2026-08-06
Open any Kubernetes manifest, GitHub Actions workflow, or Docker Compose file and you’re looking at YAML. It bills itself as a “human-friendly data serialization format,” and it genuinely is nicer to write than JSON: no quotes, no braces, no commas. The price you pay is that the parser makes a lot of implicit type guesses on your behalf — and those guesses are frequently wrong. Write no and you might get false; write version 1.10 and you might get the float 1.1. This guide walks through the YAML traps that actually hurt people in production, and the defenses that actually work.
YAML Is a JSON Superset (Mostly)
One of YAML’s design goals was to be a superset of JSON: nearly all valid JSON is valid YAML. This is both JSON and YAML:
{"name": "app", "replicas": 3, "debug": true}
There’s a useful corollary: whenever you’re unsure how to write something in YAML, you can drop into JSON syntax right inside the YAML file — add quotes and braces, and the behavior becomes unambiguous. But the “superset” claim cuts both ways. YAML permits far more than JSON does, and the same document can produce wildly different types depending on which parser reads it. When debugging a conversion, the YAML to JSON Converter is the fastest way to see what a parser actually guessed — expand the YAML to JSON and every implicit conversion becomes visible.
The Norway Problem: When no Means false
This is YAML’s most famous trap, famous enough to have a name: the Norway Problem. The YAML 1.1 specification defines a generous set of boolean synonyms: y, n, yes, no, on, off, true, false, plus their capitalized variants — all parsed as booleans.
The catch: NO is also Norway’s ISO 3166 country code. Write an unquoted list of countries:
countries:
- CN
- US
- NO
A YAML 1.1-conformant parser hands you ["CN", "US", false]. No error, no warning — the data just silently changed. The same fate awaits toggle settings named on:/off: and survey answers written as yes.
Worth being precise about the spec history: this is YAML 1.1 behavior. YAML 1.2 tightened the rules so that only true and false are booleans. But plenty of widely-used parsing libraries (older PyYAML, certain Ruby versions) still default to 1.1 semantics, so the problem is very much alive today. The only reliable defense is one rule: quote every scalar that could be a string — "NO", "no", "off" are always strings.
Version Numbers Become Floats: 1.10 Is Not 1.10
Show a YAML parser 1.10 and it will unhesitatingly treat it as a float — and the floats 1.10 and 1.1 are the same number. Your dependency pins, API versions, and release tags all get corrupted:
dependencies:
mylib: 1.10 # parsed as 1.1
The mixed case is sneakier: 1.9 becomes the float 1.9, 1.10 becomes 1.1, and v1.10 — because of the letter — stays a string. One list, three different types, and your sorting and comparison logic quietly breaks. Every version number should be a string: version: "1.10".
The Sexagesimal Ghost
YAML 1.1 has a feature almost nobody knows about: sexagesimal (base-60) numbers, modeled on clock notation. 12:34:56 parses as 12×3600 + 34×60 + 56 = 45296:
duration: 12:34:56 # YAML 1.1 parser: the integer 45296
YAML 1.2 removed this entirely — 12:34:56 is just a string there. But as long as any parser in your toolchain dates from the 1.1 era, any colon-separated number that looks like a time risks being “arithmetized.” When you see colons and digits, add quotes. No exceptions.
Anchors and Aliases: The Cost of Convenience
YAML anchors (&) and aliases (*) let you reuse blocks, and the merge key (<<) gives you something like inheritance:
defaults: &defaults
timeout: 30
retries: 3
production:
<<: *defaults
timeout: 60
This is genuinely useful in Docker Compose and CI configs, but it has three catches. First, an alias is a reference, not a copy — depending on the parser, mutating one occurrence can mutate all of them. Second, the merge key << is a 1.1-era extension rather than core spec, and support varies across language libraries. Third, and most dangerous: the “billion laughs” alias bomb — deeply nested aliases that expand exponentially in memory can take down a parsing service. Always cap alias expansion when handling untrusted YAML.
Multiline Strings: | vs >
YAML has two block scalar styles with very different behavior:
literal: |
line one
line two
folded: >
these lines
get folded
| (literal) keeps newlines — the result is "line one\nline two\n". > (folded) collapses newlines into spaces — "these lines get folded\n". Use | for SQL, shell scripts, and certificates; use > for long prose descriptions. Each also takes a chomping modifier: | keeps one trailing newline, |- strips it, and |+ keeps all of them. Embed a script in a Kubernetes ConfigMap with the wrong variant and you’ll spend an afternoon debugging a phantom trailing newline.
Tabs Are Forbidden, Indentation Is Semantics
YAML’s rule is absolute: indentation must be spaces, never tabs. A single stray tab makes most parsers error out, and the minority that accept it produce inscrutable line numbers. Worse, indentation is the structure — two extra spaces and a key silently slides into the parent’s nesting level while the file still “looks fine”:
server:
host: example.com
port: 8080 # one space short: port is now a top-level key
These errors are nearly invisible to the naked eye. Turn on “show whitespace” in your editor, agree on a team-wide indent width (2 or 4), and always actually parse a config after editing it. Viewing the parsed structure with the JSON Formatter beats counting spaces in an editor every time.
Duplicate Keys: Last One Wins, Silently
Neither the JSON nor the YAML spec firmly defines what duplicate keys mean, so most parsers chose the most dangerous option: no error, and the later value silently overwrites the earlier one:
database:
host: db1.internal
# … one hundred lines later …
host: db2.internal # this one wins; the first was never there
This bites hardest when merging large config files. Some modern parsers (certain Go libraries, Python’s ruamel.yaml in strict mode) can raise errors on duplicates — turn that on in CI if you can. When “I changed the config but nothing happened” strikes, diff the parsed structure before and after with JSON Diff; the overwritten key shows itself immediately.
Security: Never Plain-load Untrusted YAML
PyYAML’s early API is infamous: yaml.load() would by default instantiate arbitrary Python objects. A malicious YAML document could run system commands:
!!python/object/apply:os.system ["cat /etc/passwd"]
This “deserialization is code execution” problem isn’t Python-specific — Ruby and Java (certain SnakeYAML usages) have similar histories. The iron rule: for any YAML from outside your trust boundary, use a safe loader only (yaml.safe_load, SnakeYAML’s SafeConstructor). Safe loaders build only plain data structures — strings, numbers, lists, dicts — and refuse every custom type. That newer PyYAML versions now force you to name a Loader explicitly tells you how seriously the ecosystem takes this.
When to Use YAML vs JSON
They aren’t competitors; they serve different situations:
- Config files written and read by humans (CI pipelines, orchestration, app settings): YAML. Comments, multiline strings, and anchors earn their keep here.
- Data transmitted between machines (APIs, message queues): JSON. Strict type rules, fast parsing, no implicit-conversion surprises, and mature libraries everywhere.
- Want comments but need JSON-like strictness: look at JSON5 or JSONC variants rather than forcing YAML into the wrong job.
For conversion and debugging, the YAML to JSON Converter flips between the two quickly and, as a bonus, exposes every implicit type conversion — a no that became false is impossible to miss once it’s JSON.
A Defensive Validation Checklist
A practical set of defenses to finish with:
- Quote ambiguous scalars: anything that could be guessed as a boolean, number, or timestamp (
no,1.10,12:30,2024-01-01) goes in"...". - Safe loaders only: untrusted input always goes through
safe_load, with alias-expansion limits. - Schema validation: the Kubernetes ecosystem has kubeconform; for general use, convert YAML to JSON and validate with JSON Schema. Running this in CI is far cheaper than debugging in production.
- Strict mode: if your parser can error on duplicate keys, enable it.
- Parse after every manual edit: before committing hand-edited YAML, load it and compare the structure — inspect the hierarchy with the JSON Formatter and the changes with JSON Diff.
YAML’s core tension is this: to make writing pleasant for humans, it pushes enormous interpretive work onto the machine. Understand implicit typing, alias expansion, and unsafe loaders, and you can enjoy YAML’s convenience without ever being ambushed by it.