YAML Lint / Gotchas

The YAML Norway problem

YAML 1.1 promotes yes / no / on / off / Y / N to booleans without asking. A list of country codes containing NO for Norway silently becomes a list containing false. The fix is one set of quotes — but you have to know to add them.

Validate your YAML now →

The minimum reproducer

countries:
  - NO
  - SE
  - DK

Loaded with PyYAML's default safe_load, you get:

{'countries': [False, 'SE', 'DK']}

Norway is now False. Every downstream comparison ("NO" in countries) fails silently.

Why YAML 1.1 does this

YAML 1.1's "implicit typing" tries to infer types from unquoted scalars. The boolean rule is permissive:

y|Y|yes|Yes|YES|n|N|no|No|NO|true|True|TRUE|false|False|FALSE|on|On|ON|off|Off|OFF

Any plain scalar matching that regex is a boolean. The intent was readability ("write what you mean"). The cost was a class of silent data corruption that's hard to detect without tooling.

YAML 1.2's narrower rule

YAML 1.2.2 restricts booleans to:

true | True | TRUE | false | False | FALSE

Everything else is a string. Most modern parsers can opt in:

Library1.2 mode?
PyYAMLNo first-class 1.2 mode; use ruamel.yaml with typ="safe", version=(1, 2)
js-yaml v4+Default is 1.2 (FAILSAFE / CORE schema)
SnakeYAML Engine (Java)1.2 by default
go-yaml v31.2 by default
Symfony YAML1.2 since v5.4 with YAML_PARSE_NULL_AS_NULL
StrictYAML (Python)Disables implicit typing entirely — treat every value as a string until you ask otherwise

Other values that misbehave the same way

Looks likeYAML 1.1 parses asThe pain
NOboolean falseNorway country code
offboolean falseAnything labeled "off"
12:34:56integer 45296 (sexagesimal)Time-of-day, MAC addresses
0123integer 83 (octal)Zero-padded IDs
1e3float 1000.0Build IDs, version strings
2024-01-01date objectSometimes desired, often not
null / ~ / emptynullEmpty configs flip to null

The defensive rule

  1. Quote anything that's "really a string". Country codes, version numbers, IDs, env-var-style values. One pair of single quotes makes the whole class disappear.
  2. Use YAML 1.2 if your parser supports it. The default schema is much more conservative.
  3. Validate with a schema. If a field is supposed to be a string, make the schema require a string. A boolean where a string was expected is an immediate fail, not a silent corruption 200 lines later.
  4. Lint config files in CI. A linter that flags unquoted boolean-shaped values catches this before merge.

How to detect existing damage

Round-trip your config: load it, dump it back to YAML, diff. Any value that changed type during the round-trip is a candidate for quoting. For Python:

import yaml
loaded = yaml.safe_load(open("config.yml"))
print(yaml.safe_dump(loaded))
# diff against the original

Reference

Related

FAQ

Why is it called the Norway problem?

A blog post from Bram.us in 2022 popularized the name. A list of country codes 'NO: ...' for Norway gets parsed by YAML 1.1 as 'false: ...' — a single bare 'NO' becomes the boolean false. The country quietly disappears from your config.

Which parsers still do this?

By default: PyYAML (Python), libyaml-based loaders, Symfony YAML, Ruby's Psych in YAML 1.1 mode, and many CI tools that wrap them. js-yaml switched to 1.2 by default in v4. Strict-YAML and yamllint flag it. Always check your loader's spec version.

Is YAML 1.2 a fix?

It restricts booleans to true / false / TRUE / FALSE / True / False. yes/no/on/off are plain strings. But adoption is uneven — many production environments are pinned to 1.1. Quoting is still the safe answer.

What's the actual fix?

Quote any string that looks like a YAML boolean. 'NO' instead of NO. Or use a loader configured for YAML 1.2, or one that disables implicit typing entirely (StrictYAML).

Are there other implicit-type traps like this?

Yes. '12:34:56' becomes a sexagesimal integer (45296) in YAML 1.1. '1e3' becomes a float (1000.0). '0123' becomes an octal integer in 1.1, a string in 1.2. Phone numbers without quotes get reformatted. The whole class of bugs goes away if you quote anything that's 'really a string'.

Open the linter →