YAML gotchas: the Norway problem and other silent type coercions
YAML is popular because it looks like something a human would write. That is also the source of its worst behaviour: because values are unquoted, the parser has to guess what type you meant, and its guesses are occasionally very wrong.
The famous case is Norway.
countries:
- GB
- FR
- NO
Parse that with a YAML 1.1 parser and you get ['GB', 'FR', False]. Norway's ISO country code is NO, and YAML 1.1 lists no among its boolean literals. A list of strings quietly becomes a list containing a boolean, and nothing warns you.
Why it happens
YAML 1.1 defined an unusually generous set of boolean literals. All of these parse as booleans, in any capitalisation:
y yes Yes YES n no No NO
true True TRUE false False FALSE
on On ON off Off OFF
YAML 1.2, published in 2009, fixed this. Its core schema recognises only true and false.
The trouble is that a great deal of software never moved. PyYAML, still the default YAML library in the Python ecosystem, implements YAML 1.1. So does gopkg.in/yaml.v2 in Go. Your file may be perfectly valid YAML 1.2 and still be read by a 1.1 parser, and you do not usually get to choose which parser the tool you are configuring uses.
So the version question is not "which YAML do I write", it is "which YAML does this specific tool parse", and the answer is frequently 1.1.
The other coercions
Booleans get the attention, but they are not alone.
Version numbers lose precision. version: 1.10 is a float, and the float 1.10 is 1.1. Your version silently changes. version: 1.20.1 is fine, because three parts cannot be a number, which makes the failure inconsistent and therefore harder to notice.
Leading zeros mean octal. mode: 0755 parses as octal in YAML 1.1, giving 493. That happens to be what you wanted for a file mode and is a coincidence, not a feature. Meanwhile code: 08 is not valid octal, so you get an error or a string depending on the parser.
Colons can mean base 60. YAML 1.1 supports sexagesimal integers, so time: 22:30 becomes 1350, being 22 times 60 plus 30. Anything that looks like a time or a MAC address is at risk.
Empty means null. value: with nothing after it is null, not an empty string. So is ~. So is a literal null, Null or NULL.
Large numbers become floats. An integer beyond your language's precision may come back as a float and lose its tail, which matters for identifiers such as snowflake IDs.
Strings that look like dates become dates. 2026-08-29 parses as a date object, not the string you may have wanted for a key or a filename.
The fix is quoting
Every one of these disappears if you quote the value:
countries: ["GB", "FR", "NO"]
version: "1.10"
mode: "0755"
time: "22:30"
id: "12345678901234567890"
The rule worth adopting: quote any string whose value is not obviously prose. Country codes, version numbers, identifiers, times, anything with a leading zero, and any single word that could be read as yes or no. It costs two characters and removes an entire class of bug.
If you are generating YAML programmatically, use a serialiser rather than string concatenation. A good one quotes anything ambiguous automatically, which is precisely the judgement you do not want to be making by hand.
To see what a parser will actually make of a file, convert it and read the result. The YAML to JSON converter shows the parsed types explicitly, so a value that turned into a boolean or a number is immediately visible rather than lurking until deployment.
Duplicate keys
port: 8080
host: localhost
port: 9090
The specification says duplicate keys are an error. Many parsers disagree and silently keep the last one. In a long file assembled from several edits this is easy to do and very hard to spot, and the symptom is a setting that appears correct in the file and is ignored at runtime. Some linters catch it; the built in parser usually will not.
Indentation
Tabs are not permitted for indentation. YAML requires spaces, and a parser that meets a tab produces an error whose message often points at the wrong line. If your editor inserts tabs, configure it not to for .yml and .yaml files.
Because indentation is the whole structure, a single misplaced space silently reparents a key rather than failing. This is the practical argument for validating configuration files in CI rather than on deploy.
Multi-line strings
Two indicators, and the difference matters more than it looks:
literal: |
line one
line two
folded: >
line one
line two
| preserves the newlines. > folds them into spaces, giving line one line two. Use | for scripts, certificates and keys, where a folded newline corrupts the content.
Both keep a single trailing newline by default. Add - to strip it (|-) or + to keep all of them (|+). Embedding a PEM certificate with the wrong chomping indicator is a common cause of a key that looks right in the file and is rejected at load.
Anchors, aliases and the billion laughs
YAML can reference itself:
defaults: &defaults
adapter: postgres
host: localhost
development:
<<: *defaults
database: dev_db
&defaults names a node, *defaults refers to it, and << merges a mapping in. This is genuinely useful for reducing repetition in configuration.
It is also an amplification primitive. A small file with nested aliases, each referring to the previous one several times, expands exponentially during parsing and exhausts memory. This is the billion laughs attack, and it applies to YAML exactly as it does to XML entities. Any parser handling untrusted input needs limits on expansion.
Never load untrusted YAML unsafely
YAML can encode language specific objects with tags, and some parsers will instantiate them. In Python, yaml.load with the default loader has historically been able to construct arbitrary objects, which turns parsing a YAML file into remote code execution.
Use yaml.safe_load in Python. Use UnmarshalStrict and known types in Go. In every language, use the parser's safe or schema restricted mode for anything you did not write yourself. This is not a theoretical hardening step; it is the single most exploited weakness in YAML handling.
A short checklist
- Quote country codes, versions, identifiers, times and anything with a leading zero.
- Know whether your tool's parser is YAML 1.1 or 1.2, and assume 1.1 when unsure.
- Lint configuration files in CI, including a duplicate key check.
- Use
|not>for keys, certificates and scripts. - Use the safe loader for any input you did not author.
- Convert and inspect the parsed output when a value behaves oddly, rather than rereading the YAML.
Related reading: JSON versus TOML for config compares the alternatives when YAML's flexibility is more cost than benefit.