YAML’s sharp edges: the Norway problem, tabs, and silent type coercion

YAML is readable until it is not. The Norway problem, version numbers that become floats, tabs that are illegal, and indentation that changes meaning — the failure modes worth knowing before they reach production.

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.

What you wrote / what the parser produced
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.

Inconsistent by design
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

All of these are null
a: null
b: Null
c: ~
d:
e: NULL

The 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 versus folded
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.

Anchor, alias, and merge
defaults: &defaults
  timeout: 30
  retries: 3

production:
  <<: *defaults
  timeout: 60

Aliases 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

  1. 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.
  2. Never indent with tabs. Configure your editor to expand them in .yml and .yaml files.
  3. Use the pipe, not the greater-than, for anything where line breaks are significant — keys, certificates, scripts.
  4. Use the safe loader for input you did not author.
  5. 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.