YAML was designed to be human-friendly, and it succeeds — right up until one of five spec-compliant behaviours turns a boolean into a country code and takes production down with it.

YAML is the config format everyone reaches for because it's the one you can read without documentation. Indentation, colons, lists — it looks like structured English. As Wikipedia sums it up:

"YAML is a human-readable data serialization language. It is commonly used for configuration files and in applications where data is being stored or transmitted."

— Wikipedia, "YAML" (CC BY-SA 4.0)

That readability is real, and it's also a trap, because a handful of YAML behaviours are perfectly valid, completely undocumented in your onboarding, and responsible for a wildly disproportionate share of "but the config looks fine" incidents. Here are the five that get people.

1. The Norway problem

Write a list of country codes in YAML and set one of them to Norway's ISO code, NO. In YAML 1.1 — which most parsers still default to — that value parses as the boolean false. Norway vanishes from your config and becomes a falsehood. It's not just NO: yes, no, on, off, true, and false are all coerced to booleans unless quoted. YAML 1.2 removed most of this, but your parser probably hasn't caught up. The defence is a habit: quote any string that could be mistaken for a boolean. country: "NO" stays Norway.

2. Implicit number typing

YAML guesses types, and its guesses have edges. version: 1.0 is a float, so it becomes 1 and your version comparison breaks. version: 1.10 parses as the float 1.1 — the trailing zero evaporates. A leading-zero value like an account number or a US ZIP code can be read as octal or stripped. The only safe rule for anything that isn't genuinely a number you'll do math on: quote it. version: "1.10" is a string and stays exactly what you wrote.

3. Multiline strings: two flavours, four endings

YAML has two block-string styles and they mean different things. | (literal) keeps your newlines; > (folded) collapses them into spaces. Then each takes a chomping modifier: | keeps one trailing newline, |- strips it, |+ keeps all of them. Get this wrong in a shell script embedded in a Kubernetes manifest and you get invisible trailing-whitespace bugs that are miserable to debug because the character that's breaking things doesn't render. When a multiline value misbehaves, the block indicator is the first place to look.

4. Anchors and aliases: shared state in disguise

YAML lets you define a block once with &anchor and reuse it with *alias, which is genuinely useful for DRY config. The gotcha: in some parsers the alias isn't a copy, it's a reference to the same object. Mutate one and you mutate every place the anchor was used, because they're the same underlying node. Config that looks like three independent environments can quietly be one object wearing three names. Powerful feature, sharp edge — use anchors for constants, be very careful using them for anything a later step modifies.

5. Tabs are illegal, and the error won't say so

YAML does not permit tab characters for indentation. Full stop. The painful part isn't the rule — it's that a single stray tab, often pasted in from an editor that didn't convert it, produces a parse error that frequently points at the wrong line or just says "unexpected character" with no useful location. Whole afternoons have died to one invisible tab. If a YAML file refuses to parse and everything looks perfectly indented, suspect a tab before you suspect anything else.

Merge keys: convenient until they're not

YAML's merge key (<<: *alias) lets you merge an anchored mapping into another mapping, which looks like inheritance for config blocks. Define a &defaults block with shared settings and merge it into each environment — clean, DRY, readable. The trap is that merge keys are not part of the YAML 1.2 specification; they're a YAML 1.1 extension that some parsers support and others silently ignore. Code that works in PyYAML (which supports merge keys) can silently drop the merged fields in a stricter parser. And when two merged anchors define the same key, the resolution order isn't always obvious — the last one wins, but "last" depends on the order of the merge list, which isn't always the order you read it in. Merge keys are powerful in controlled environments where you know the parser, and a silent failure waiting to happen in cross-tool pipelines.

YAML and security: the deserialization risk

In several YAML libraries — most infamously PyYAML's yaml.load() without a Loader argument — the parser can instantiate arbitrary objects from YAML tags, which means a malicious YAML file can execute code on the machine that parses it. This is not a bug in YAML itself; it's a consequence of YAML's tag system being powerful enough to construct language-level objects, combined with libraries defaulting to the most permissive loader. The fix is to always use yaml.safe_load() (or the equivalent safe loader in your language), which restricts parsing to basic data types. Any application that accepts YAML from untrusted sources — user uploads, webhook payloads, CI pipeline definitions — and parses it with the full loader is running user-supplied code. This has been exploited in real-world attacks against CI systems and configuration management tools.

When to just use JSON instead

YAML's readability advantage is real for hand-edited configuration. But for machine-to-machine data exchange, API payloads, or any context where humans rarely read the raw file, JSON is simpler and safer: no implicit typing, no indentation semantics, no anchors, no merge keys, no tab ambiguity, and a parser that can't execute arbitrary code. If your YAML file is being generated by a script and consumed by another script, with no human editing in between, you're paying YAML's complexity tax for a readability benefit nobody collects. The honest question is: will a human regularly edit this file by hand? If yes, YAML's readability earns its keep. If no, JSON removes an entire class of bugs for free.

The reliable defence: convert and inspect

The common thread across all five is that YAML's failures are invisible — the file looks right and parses wrong. The fastest way to catch that is to stop trusting your eyes and look at what the value actually became. Round-trip the file through a YAML to JSON converter: the moment NO shows up as false or 1.10 as 1.1, the JSON output makes it obvious in a way the YAML source never will. Pretty-print the result with a JSON formatter to read the parsed structure cleanly, and when a config change breaks something, run the before and after through a text diff to see exactly which value shifted. YAML is fine. It just needs verifying, not trusting.

← All articles