A team ships a country allowlist in YAML. It works in every test. Then a customer in Norway can't check out, and it takes an afternoon to find out why: the config had country: no, and the YAML parser read no as the boolean false, not the string "NO". Nobody wrote a bug — the format did. That's the "Norway problem," and it's the single best argument for treating config-format choice as an engineering decision rather than an aesthetic one.
The formats don't differ mainly in how they look. They differ in what silently goes wrong, and each has a signature failure.
The mistakes each format sets you up for
YAML coerces values you meant as strings
YAML tries to guess types, and the guesses bite. country: no and answer: yes become booleans. A version pin like version: 1.20 parses as the float 1.2 — the trailing zero vanishes. A ZIP code 07030 may lose its leading zero or, with an 8 or 9 in it, error as an invalid octal. YAML 1.2 narrowed these implicit conversions, but many popular libraries (PyYAML among them) still default to 1.1 behavior, so you can't assume you're safe. The fix is boring and reliable: quote anything that could be read as a bool, number, or date — country: "no".
YAML's whitespace is load-bearing and invisible
Indentation defines structure, so a stray tab (YAML forbids tabs for indentation and rejects the file) or a misaligned block silently reparents a key. A block scalar folded the wrong way turns a multi-line string into something you didn't write. This is why generated YAML is safer than hand-edited YAML at scale, and why every large YAML config eventually gets a schema check in CI.
JSON has no comments and no trailing commas — by design
You cannot annotate a JSON config, and the moment someone adds // TODO or leaves a trailing comma after the last array element, the parser rejects the whole file. Per RFC 8259 both are simply not part of the grammar. "JSON with comments" (JSONC, JSON5) is a different format your standard parser won't accept — don't assume JSON.parse will read your editor's tolerant version.
TOML gets ugly once nesting goes deep
TOML is genuinely pleasant for flat, hand-edited settings — a Cargo.toml, a pyproject.toml. But deeply nested structures turn into a thicket of [section.subsection.item] headers and [[arrays.of.tables]], and it stops being the readable format it was chosen for. If your config is more than two levels deep in most places, TOML is fighting you.
Choosing on failure mode, not looks
| If you need | Pick | Because |
|---|---|---|
| A machine contract / API payload | JSON | Unambiguous parse, universal support, strict grammar |
| Small hand-edited settings with comments | TOML | Comments, no type-guessing surprises, flat-friendly |
| Kubernetes, GitHub Actions, Ansible | YAML | The ecosystem gives you no choice |
| Deeply nested hand-edited data | JSON + schema | Nesting stays legible; validate on load |
There's a defensive rule under all of this: your format should make an invalid config fail loudly at startup or in CI, not fail quietly in production three weeks later like the Norway bug. That means a JSON Schema, a load-time validation step, and quoting ambiguous YAML values on principle. The format you pick matters far less than whether a bad value gets caught before a customer does — a converter like the YAML to JSON converter can also expose these coercions early by showing you exactly how a parser typed each value.
Cover photo by Stanislav Kondratiev on Pexels.
