Skip to content
Control Plane Labs

The Same YAML, Eight Different Answers

We parsed 22 tricky YAML values with eight parsers. NO, on, y, 0644 and duplicate keys came back differently, and Kubernetes turned a key y into true.

Ben Ennis

Published October 20, 2026

A ConfigMap ships with COUNTRY: NO. kubectl apply fails with cannot unmarshal bool into Go struct field ConfigMap.data of type string. Someone quotes the value, the deploy goes through, and the team moves on. A month later a Helm chart sets a coordinate with an unquoted key y, and nothing fails at all. The data lands in the cluster under the key "true".

Both are the same bug. The YAML was valid, and the parser did exactly what its rules say. The rules depend on which parser you use. To see how far apart they are in practice, we fed the same 22 snippets to eight YAML libraries that developer tooling depends on and recorded what each one returned.

Why parsers disagree

YAML 1.1, from 2005, resolves a long list of plain words as booleans. The YAML 1.1 boolean type accepts y|Y|yes|Yes|YES|n|N|no|No|NO|true|True|TRUE|false|False|FALSE|on|On|ON|off|Off|OFF. Its integer rules treat a leading zero as octal, allow _ separators, and read 1:30 as base 60. It also defines the << merge key.

YAML 1.2 replaced that with a much smaller core schema. Only true and false, in three capitalizations, are booleans. Octal needs a 0o prefix. If none of the core patterns match, “the scalar is resolved to tag:yaml.org,2002:str.” Merge keys aren’t part of 1.2 at all.

Libraries picked a side at different times, and many never moved. The result is that “YAML” names a family of slightly different languages, and the one you get depends on the parser.

The test

We ran each case through each library’s default safe loader with default options on October 20, 2026:

The last one is what Kubernetes uses. The Kubernetes v1.37.1 go.mod pins sigs.k8s.io/yaml 1.6.0 and go.yaml.in/yaml/v2 2.4.4. The library’s README says it “first converts YAML to JSON using go-yaml and then uses json.Marshal and json.Unmarshal.” Every case and every raw result is published as JSON.

Results

Input PyYAML ruamel js-yaml 3 js-yaml 5 yaml 2 go-yaml v2 go-yaml v3 Kubernetes
country: NO false “NO” “NO” “NO” “NO” false “NO” false
enabled: on true “on” “on” “on” “on” true “on” true
answer: y “y” “y” “y” “y” “y” true “y” true
on: as a key true “on” “on” “on” “on” “true” “on” “true”
mode: 0644 420 644 420 644 644 420 420 420
mode: 0o644 “0o644” 420 “0o644” 420 420 420 420 420
zip: 08080 “08080” 8080 “08080” 8080 8080 8080 8080 8080
duration: 1:30 90 “1:30” 90 “1:30” “1:30” “1:30” “1:30” “1:30”
limit: 1e3 “1e3” 1000 1000 1000 1000 1000 1000 1000
count: 1_000 1000 1000 1000 “1_000” “1_000” 1000 1000 1000
date: 2026-10-20 date date Date string string string time string
Duplicate key a last wins error error error error last wins error last wins
<< merge key merged merged merged literal key literal key merged merged merged

A few cases agreed everywhere. version: 1.10 became the number 1.1 in all eight, which loses the trailing zero. 0x1F was 31 everywhere, ~ and Null were null everywhere, and .inf parsed as infinity everywhere. Kubernetes then rejected .inf when converting to JSON (json: unsupported value: +Inf). The three JavaScript libraries also turned 12345678901234567890 into 12345678901234567000, because JavaScript numbers are 64-bit floats.

What the table says

No two columns match. Even libraries that claim the same YAML version differ. go-yaml v3 reads NO as a string, like YAML 1.2, but still treats 0644 as octal, accepts 1_000, and applies merge keys. PyYAML and js-yaml 3 both read 0644 as octal, but only PyYAML rejects 1e3 as a number, because YAML 1.1 floats need a decimal point.

Kubernetes behaves like YAML 1.1. Its column matches go-yaml v2, booleans and all, because sigs.k8s.io/yaml is built on the v2 fork. y, n, yes, no, on, and off are all booleans in Kubernetes manifests. That includes keys. JSON keys must be strings, so a boolean key gets stringified to "true" or "false".

Some failures are loud and some are silent. We decoded test ConfigMaps with k8s.io/apimachinery 0.37.1 to see both paths:

  • COUNTRY: NO converts to the JSON "COUNTRY": false. Strict JSON decoding into a ConfigMap then fails with cannot unmarshal bool into Go struct field ConfigMap.data of type string. That is the familiar error, and it at least stops the deploy.
  • PORT: 8080 fails the same way, with cannot unmarshal number.
  • y: "2" converts to "true": "2". The value is a valid string, so nothing fails. The ConfigMap is stored with the wrong key.
  • Decoding the same YAML straight into a typed Go struct, which many controllers and operators do, didn’t fail at all. COUNTRY came back as the string "false" and PORT as "8080".

The loud cases get fixed in the first deploy. The silent ones, a renamed key or a value that is now the text “false,” get found in production.

Duplicate keys are a coin flip. PyYAML, go-yaml v2, and Kubernetes keep the last value without a warning. The other five refuse to parse. A file that one tool accepts can fail in another, and a merge conflict resolved badly can keep the wrong value without anyone noticing.

Merge keys are not portable. js-yaml 5 and yaml 2 kept << as an ordinary key in our default-options test. The yaml docs say merge support “depends on YAML version” and is off by default for 1.2, because “merge keys are a YAML 1.1 feature that is not a part of the 1.2 spec.” Anchors and << that work in a Python or Go pipeline can silently produce a different structure in a Node.js tool.

The GitHub Actions on: case

The on: row explains a puzzle many people have hit. GitHub Actions workflows start with on:. GitHub’s own parser reads that as the string “on,” which is how the workflow syntax reference uses it. Load the same file with PyYAML to lint or generate it, and the key comes back as the boolean True. Write it back out, and the key becomes true:, which breaks the workflow. Tools that rewrite workflow files need a YAML 1.2 parser such as ruamel.yaml, or they need to quote the key as "on":.

How to avoid it

  1. Quote strings that could look like anything else. Country and language codes, yes/no flags meant as text, version numbers, zip codes, file modes, times, and single-letter keys like y and n. "NO", "1.10", "0644", and "y": are unambiguous in every parser we tested.
  2. Write file modes as decimal in Kubernetes. For defaultMode on Secret and ConfigMap volumes, write 420, not 0644. Both work today in Kubernetes, but 0644 is 644 in a YAML 1.2 tool that edits the file before it reaches the cluster.
  3. Don’t rely on << in files that more than one tool reads. Expand anchors in generated output, or confirm every consumer supports merge keys.
  4. Fail on duplicate keys in CI. Run a strict parser such as ruamel.yaml or yamllint’s key-duplicates rule over your repository, so a duplicate fails a check instead of choosing a value.
  5. Check what Kubernetes actually stored. After an apply, compare kubectl get configmap app -o json with the source file. JSON shows the key and value exactly as stored, so a "true" key or a "false" value is easy to see.
  6. Consider KYAML for Kubernetes output. The KYAML guide covers the Kubernetes output format that double-quotes all value strings to avoid exactly this class of bug.

To see how a snippet converts before it ships, paste it into the YAML to JSON converter. JSON makes every type explicit. For Kubernetes manifests, the Kubernetes YAML linter catches common structural mistakes as well.

Frequently asked questions

Why does YAML turn NO into false?+
YAML 1.1 treats y, n, yes, no, on, off, true, and false, in several capitalizations, as booleans. Parsers that follow YAML 1.1, including PyYAML and the library Kubernetes uses, read an unquoted NO as false. YAML 1.2 parsers read it as the string NO.
Does Kubernetes use YAML 1.1 or 1.2?+
In our test of sigs.k8s.io/yaml 1.6.0, the version pinned by Kubernetes 1.37.1, it behaved like YAML 1.1: y, yes, on, and NO became booleans, 0644 was octal, and duplicate keys kept the last value.
Why did my ConfigMap key y become true?+
Kubernetes reads an unquoted y as the boolean true. JSON keys must be strings, so the key is stored as true. Quote the key as "y": to keep it.
Is 0644 octal in YAML?+
It depends on the parser. In YAML 1.1 parsers it is octal, so 420 in decimal. In YAML 1.2 core schema parsers it is the decimal 644, and octal needs the 0o prefix, as in 0o644.
Are duplicate keys allowed in YAML?+
The YAML spec requires mapping keys to be unique, but parsers differ. In our test, ruamel.yaml, go-yaml v3, and the three JavaScript libraries rejected duplicates, while PyYAML, go-yaml v2, and Kubernetes silently kept the last value.
How do I make YAML parse the same everywhere?+
Quote any string that could be read as a boolean, number, date, or null; avoid merge keys in shared files; write Kubernetes file modes in decimal; and check files in CI with a strict parser that rejects duplicate keys.

Tags: #yaml, #json, #kubernetes, #parsers, #norway-problem, #incident, #original-data