Data formats
Why YAML quietly turns Norway into false
A country code, a version number and a timestamp walk into a config file. The type-inference rule behind one of the most persistent bugs in YAML, and the two characters that prevent it.
Configuration files break in quiet ways. A value that looks right, reads right and passes review turns into something else the moment a parser touches it. The best-known example has a name — the Norway problem — and it is worth understanding properly, because the rule behind it affects far more than one country code.
What actually happens #
Here is a country list in YAML:
countries:
- GB
- FR
- DE
- NO
Parse it, and you get this:
{ "countries": ["GB", "FR", "DE", false] }
Norway has become the boolean false.
The cause is type inference. YAML tries to work out what you meant from unquoted text, and in the YAML 1.1 specification the tokens y, yes, n, no, on and off are all valid ways of writing booleans. NO is Norway's ISO 3166 country code. It is also, to a YAML 1.1 parser, the word "no".
The bug is nasty because nothing fails. No error, no warning. Your country dropdown simply has a false in it, and somebody notices weeks later when a Norwegian customer cannot complete checkout.
It is not just Norway #
The same rule catches several other things that turn up constantly in real configuration.
| You write | YAML 1.1 reads | Should have been |
|---|---|---|
NO | false | Norway |
ON | true | Ontario |
y | true | the letter y |
1.10 | 1.1 | version 1.10 |
08 | error or 8 | the month August |
12:30 | 750 | half past twelve |
~ | null | a tilde |
Yes | true | the word Yes |
The version number case deserves attention because it is so common in CI configuration. Writing version: 1.10 gives you the float 1.1, and the trailing zero is gone for good. If that value goes into a Docker tag or a package constraint, you are now pinning something you did not intend.
The 12:30 case is stranger still: YAML 1.1 supports sexagesimal — base 60 — integers, so a time reads as a number of seconds. Almost nobody knows this until it happens to them.
Why YAML 1.2 did not fix it #
YAML 1.2, published in 2009, narrowed the boolean type to exactly true and false. Under a strict 1.2 parser, NO is a string and the problem does not exist.
In practice this helps less than it should, because a large amount of deployed tooling still implements 1.1 behaviour. PyYAML's default loader — the one most Python code reaches for — follows 1.1. So do several widely used parsers in other languages. You cannot assume which specification a downstream consumer implements, and configuration files rarely stay in one runtime.
The practical position: do not rely on the specification version to protect you.
The fix #
Quote anything that could be read as something else.
countries:
- "GB"
- "FR"
- "DE"
- "NO" # quoted, and stays a string everywhere
version: "1.10"
start: "12:30"
Quoting costs two characters and removes an entire class of bug. For a value that is semantically a string — a code, an identifier, a version, a time — quote it as a matter of course, not only when you suspect a problem.
If you want to see what a parser actually reads rather than what you intended, run the file through the YAML to JSON converter. JSON has no type inference, so whatever comes out is unambiguous. A false where you expected "NO" is immediately visible.
Going the other way, the JSON to YAML converter quotes these values for you. It quotes any scalar a parser could reinterpret — reserved words, anything numeric-looking, strings with structural characters — so output from it survives a round trip through any parser without changing meaning.
What to do in a codebase #
Three habits prevent most of this.
Quote by default in data. In a list of codes, identifiers or versions, quote every entry rather than only the suspicious ones. Consistency is easier to review than judgement, and the reviewer who spots an unquoted NO in a hundred-line list is rare.
Validate against a schema. A JSON Schema that declares countries as an array of strings rejects a boolean immediately. This catches the problem at the point it is introduced rather than in production. The JSON schema generator will infer a starting schema from a sample document.
Use a strict loader. In Python, yaml.safe_load still follows 1.1; if you need 1.2 semantics, ruamel.yaml in its default mode gets you there. Whatever your language, find out which specification your parser implements before assuming.
The broader point #
YAML is pleasant to write because it guesses what you meant. That is the same property that makes it possible to write something ambiguous without noticing. The format optimises for the person typing, and the cost lands on the person debugging six weeks later.
None of this is an argument against YAML. It is an argument for quoting strings, validating structure, and checking what a parser actually produces rather than trusting what the file looks like. All three take minutes. The Norway problem, undiagnosed, takes considerably longer.
Frequently asked questions
Does this affect YAML 1.2?
Officially no — YAML 1.2 narrowed booleans to exactly true and false. But a great deal of deployed tooling still implements 1.1 behaviour, including PyYAML's default loader, so you cannot rely on the specification version to protect you.
Which other values does this catch?
y, n, yes, no, on and off all parse as booleans. Version numbers like 1.10 become the float 1.1. Times like 12:30 become 750 under YAML 1.1's sexagesimal integers. Leading-zero values like 08 error or lose the zero.
What is the fix?
Quote it. Any value that is semantically a string — a code, an identifier, a version, a time — should be quoted as a matter of course rather than only when you suspect a problem.