Published · 7 min read
YAML won the configuration format war because it is pleasant to read. Kubernetes manifests, CI pipelines, Docker Compose files, and Ansible playbooks are all YAML, which means most developers write it daily.
Its readability comes from doing a great deal of inference on your behalf, and that inference has edge cases sharp enough to have their own names. None of them produce an error — they produce a value you did not intend, which is considerably worse.
The Norway problem
The best known YAML bug. In YAML 1.1, the bare words yes, no, on, off, y, and n are booleans. A list of country codes therefore does something surprising.
countries: countries:
- GB - "GB"
- NO - false
- FR - "FR"Norway becomes false. The document parses cleanly, nothing warns you, and the failure surfaces somewhere far away as a country code that is not a string.
YAML 1.2 narrowed booleans to true and false only, but many widely used parsers still implement 1.1 semantics or a hybrid. Assuming the strict behaviour is not safe. Quote anything that could be read as a boolean.
Version numbers become floats
Write a version as 1.20 and you get the number 1.2, because a bare 1.20 is a float and trailing zeros are not significant in floats. Write 1.2.3 and you get a string, because it is not a valid number. So version pinning behaves differently depending on how many components the version has.
version: 1.20 → 1.2 (number)
version: 1.2.3 → "1.2.3" (string)
version: "1.20" → "1.20" (string) ✓Leading zeros and sexagesimals
In YAML 1.1, a number with a leading zero is octal. So 0755 is 493, which is at least defensible for a file mode, and a zip code written as 07030 is a syntax error or a different number depending on the parser.
YAML 1.1 also has base-60 numbers, a feature intended for times. It means 12:30 can parse as 750 rather than a string. This one is rare enough to be a genuine surprise when it happens, and it is why time values should always be quoted.
Tabs are illegal
YAML forbids tabs for indentation. Not discouraged — forbidden by the specification, because tab width is a display setting and YAML derives structure from indentation, so a tab has no defined structural meaning.
The error message usually points at a line other than the one containing the tab, since the parser fails when the indentation becomes inconsistent rather than at the character itself. If a YAML file will not parse and looks correct, search for a literal tab before anything else.
Null has five spellings
a: null
b: Null
c: ~
d:
e: NULLThe fourth is the one that catches people: a key with nothing after the colon is not an empty string, it is null. A commented-out value leaves the key present and null rather than absent, and code that checks whether a key exists will behave differently from code that checks whether it has a value.
Multi-line strings and the indicator characters
YAML has two block scalar styles and they differ in how they treat newlines. The pipe preserves them; the greater-than folds them into spaces. Each takes an optional chomping indicator controlling the trailing newline: minus strips it, plus keeps all of them, and the default keeps exactly one.
literal: | folded: >
line one line one
line two line two
→ "line one\nline two\n" → "line one line two\n"This matters more than it looks. An SSH key or a PEM certificate pasted into a folded scalar has its newlines replaced by spaces and becomes unusable, while looking perfectly correct in the file.
Anchors and aliases
YAML can define a node once and reference it elsewhere — an anchor with & and an alias with *, plus a merge key to splice a map into another. It is genuine deduplication, and it is why YAML config files can avoid the copy-paste that JSON forces.
defaults: &defaults
timeout: 30
retries: 3
production:
<<: *defaults
timeout: 60Aliases are also the mechanism behind the billion laughs attack: nested aliases that each reference the previous one expand exponentially and exhaust memory. Never parse untrusted YAML with an unbounded parser.
Never parse untrusted YAML with a full loader
This is the one that is a security issue rather than an inconvenience. Full YAML supports language-specific tags that instruct the parser to construct arbitrary objects — which in several languages means invoking arbitrary code during parsing.
In Python, yaml.load with the default loader was remote code execution for years, which is why yaml.safe_load exists and why the unsafe default was eventually changed. Ruby's Psych and several Java YAML libraries have had equivalent issues. Use the safe loader, always, for anything you did not write yourself.
The practical rules
- Quote any string that could be read as something else: country codes, version numbers, times, values with leading zeros, and anything that is yes, no, on, or off.
- Never indent with tabs. Configure your editor to expand them in .yml and .yaml files.
- Use the pipe, not the greater-than, for anything where line breaks are significant — keys, certificates, scripts.
- Use the safe loader for input you did not author.
- When behaviour is surprising, convert the YAML to JSON and look at what the parser actually produced. It removes all doubt in about five seconds.
That last one is the fastest debugging technique for any of the problems above, because JSON has no inference — what you see is exactly what was parsed.
Frequently asked questions
- Is JSON valid YAML?
- YAML 1.2 is a strict superset of JSON, so any valid JSON document is valid YAML and can be pasted straight into a YAML file. YAML 1.1 predates that guarantee and has minor incompatibilities.
- Should I use YAML or TOML for configuration?
- TOML has a much smaller specification, no significant whitespace, and unambiguous types, which eliminates most of this article. YAML handles deep nesting more gracefully and has anchors. For flat-to-moderate config, TOML has fewer ways to surprise you; for deeply nested structures, YAML reads better.
- Why does my Kubernetes manifest fail with no useful error?
- Most often indentation — a list item indented one level too far becomes a child of the wrong key, which is structurally valid YAML and semantically wrong. Convert it to JSON and check that the shape is what you intended.