YAML's indentation-as-syntax and implicit type inference make it readable to write and dangerous to get subtly wrong — here's why 'NO' becomes false, why tabs are banned, and why 1.10 silently becomes 1.1.
A Kubernetes manifest that deploys fine in staging and silently skips a container in production. A CI pipeline step that "runs" but does nothing, because a key got demoted from a mapping to a scalar. A config value that's the string "12345" in one file and the integer 12345 in another, and a downstream parser that only handles one of them. None of these are YAML bugs in the sense of a parser doing the wrong thing. They're YAML doing exactly what the spec says, using whitespace rules most people never read.
YAML's pitch was always "JSON, but for humans" — no braces, no trailing commas, no quote-everything ceremony. The cost of that readability is that structure lives in invisible characters: how many spaces indent a line, whether a scalar got auto-typed, whether a block ended where you thought it did. This is a tour of the specific whitespace and typing rules that keep causing outages, and why the spec is built this way in the first place.
In JSON, whitespace is decoration. {"a":1} and { "a" : 1 } parse identically. In YAML's block style, indentation is the delimiter — it plays the role that {, }, and , play in JSON. Two mappings at different indent levels are different nesting depths, full stop.
service:
name: api
ports:
- 8080
env:
- NODE_ENV=production
Here ports and env are both children of service because they share the same indentation (2 spaces). Shift env: one space to the right and it silently becomes a child of ports instead of a sibling — and because ports was expecting a sequence, not a mapping, some parsers will throw, but others will just produce a structure you didn't intend and don't notice until something downstream reads a missing key.
The rule that trips people up hardest: tabs are not allowed for indentation, anywhere, in block context. This isn't a style-guide preference, it's in the YAML 1.1 spec and carried into 1.2 — tabs are reserved as separators in other contexts and mixing them into indentation makes the "how deep is this line" calculation ambiguous across editors with different tab-width settings. Most parsers reject a literal tab character used for indentation outright, which is actually the better failure mode — a loud parse error beats a document that indents differently depending on whether your editor renders tabs as 2, 4, or 8 columns.
YAML doesn't require quotes around scalars, which means every unquoted value gets run through a type-resolution step that guesses whether it's a string, number, boolean, or null. YAML 1.1 — which is what most real-world parsers (PyYAML's default loader, older Ruby Psych, many Go libraries) actually implement — has an unusually generous idea of what counts as a boolean:
| Unquoted value | YAML 1.1 resolves to |
|---|---|
no, No, NO | false |
yes, Yes, YES | true |
on, On, ON | true |
off, Off, OFF | false |
null, ~, empty | null |
NO (as in Norway's ISO 3166 country code) | false |
That last row is the famous one — the "Norway problem." A YAML file listing country codes:
countries:
- NO
- SE
- FR
produces [false, "SE", "FR"] under a YAML-1.1-compliant loader, because NO matches the boolean pattern before it's considered a plain string. This has broken real config files and CI pipelines, not as a hypothetical — it's referenced directly in the YAML 1.2 Core Schema rationale for why 1.2 narrowed the boolean set down to just true/false. The catch: your parser's default schema determines which spec version's rules apply, and plenty of production systems still run 1.1-schema parsers, so "we upgraded YAML" in your head doesn't mean the loader agrees.
The fix that always works, regardless of schema version, is to quote anything that isn't unambiguously the type you want: "NO", "yes", "on". Quoting a string in YAML costs nothing and removes it from type inference entirely.
Version fields are the classic casualty:
version: 1.10
A human reads 1.10 as "one point ten." A YAML float parser reads it as 1.1 — the trailing zero after a decimal point isn't semantically significant to a numeric type, so it's silently dropped in the in-memory representation, and depending on how the consumer serializes it back out, 1.10 and 1.1 can end up indistinguishable. Docker Compose files, Helm charts, and Kubernetes API versions all hit this; the standard workaround is the same as above — quote it: version: "1.10".
Octal and sexagesimal (base-60!) number formats are another 1.1-vs-1.2 landmine. Under YAML 1.1, 010 parses as octal 8, and colon-separated numbers like 1:30:00 parse as a base-60 integer (5400). YAML 1.2's core schema drops both of these in favor of a stricter, JSON-compatible number grammar. If your config has a leading-zero string that's meant to stay a string — a zip code, a phone extension, a version segment — quote it, always, on every parser, on every YAML version.
YAML also supports "flow style," which is just JSON syntax embedded inside a YAML document:
ports: [8080, 8443]
env: {NODE_ENV: production, LOG_LEVEL: debug}
Flow style sidesteps indentation-as-structure entirely — braces and brackets delimit scope explicitly, same as JSON, so the Norway problem's typing rules still apply (an unquoted on inside flow style is still a boolean) but indentation bugs can't happen inside a flow collection. This is why tools that generate YAML programmatically (Helm's --set, kubectl output, most YAML libraries' default dump behavior for nested structures) often fall back to flow style for anything past a certain nesting depth — it's less pretty but structurally unambiguous. If you're hand-writing something with deep nesting and keep getting indentation wrong, flow style for the deepest levels is a legitimate escape hatch, not a hack.
YAML has two block scalar indicators for literal multi-line text — | (literal, preserves newlines as-written) and > (folded, converts single newlines to spaces) — each modifiable with - (strip trailing newline) or + (keep all trailing newlines). Getting these confused is a routine source of bugs in anything that embeds a script or certificate in YAML (CI configs, Kubernetes ConfigMaps):
literal: |
line one
line two
folded: >
line one
line two
literal produces "line one\nline two\n". folded produces "line one line two\n" — the newline between the two source lines becomes a space, which is exactly what you want for a wrapped paragraph and exactly what you don't want for a shell script or PEM certificate, where > will silently join lines that needed to stay separate. The failure mode here is quiet: the YAML parses fine, the string comes out some value, and it's only wrong once something downstream tries to execute or verify it.
None of this is an accident or an oversight — it's the direct cost of YAML's founding goal of being a strict superset-adjacent, human-editable format that still maps cleanly onto native data structures in Perl, Python, and Ruby (YAML's original target languages, circa 2001). JSON made the opposite trade: mandatory quotes and braces buy unambiguous parsing at the cost of hand-editing comfort. TOML, which showed up later specifically as a reaction to YAML's implicit-typing complexity, makes a third trade — explicit types like JSON, but a flatter, more restrictive structure than YAML's arbitrary nesting, which is why it's become the default for tool configs (Cargo, pyproject.toml) where the schema is shallow and known in advance, while YAML keeps its grip on deeply nested, human-authored infrastructure config (Kubernetes, CI pipelines, Ansible) where the nesting is the point.
If you write YAML professionally, the practical checklist is short: never use tabs for indentation, quote anything that isn't obviously the type you want (especially country codes, version numbers, and yes/no-shaped strings), know whether your parser defaults to 1.1 or 1.2 core-schema typing, and pick | vs > deliberately rather than by habit. Running a document through a YAML validator before it hits a pipeline catches the indentation and syntax class of bug immediately — it won't stop the Norway problem, since NO is valid YAML that just resolves to a type you didn't want, but it will catch the malformed-structure failures before they reach production.