The YAML + JSON config reference
How YAML and JSON really differ, why YAML 1.1 versus 1.2 keeps breaking configs, and the type coercion traps that bite Kubernetes, Helm, and Ansible users.
Ben Ennis
Published July 27, 2026
If you spend your day in Kubernetes manifests, Helm values files, GitHub Actions workflows, and Ansible playbooks, you are reading two data languages with very different philosophies. JSON is small enough to specify in a dozen pages and has almost no surprises. YAML is designed for humans to write and pays for that with a resolution algorithm that guesses types, and with a version split that most people do not know exists.
This is the reference for the parts that actually cause outages.
The two specs you are actually using
JSON is RFC 8259, which supersedes RFC 7159 and 4627. It is short, it mandates UTF-8 for interchange between systems, and it deliberately leaves a few things undefined.
YAML has a version problem. The current spec is
YAML 1.2.2, a 2021 revision of the 2009 1.2
release, and its stated goal was making YAML a strict superset of JSON. But an
enormous amount of deployed tooling still implements
YAML 1.1, including PyYAML, which is what Ansible
and most Python tooling use — see the
PyYAML documentation. Go’s
gopkg.in/yaml.v3 and JavaScript’s
js-yaml are closer to 1.2.
The practical consequence: the same file can mean different things depending on which parser reads it. That is not a hypothetical. It is the root cause of the two most common YAML bugs below.
Implicit typing, the whole problem in one section
YAML resolves unquoted scalars to types by pattern matching. In YAML 1.1 the
boolean pattern is broad. The
1.1 bool type definition resolves y, Y,
yes, Yes, YES, n, N, no, No, NO, on, off, true, and
false — and their case variants — to booleans. YAML 1.2’s core schema narrows
this to true and false only.
# YAML 1.1 parser: all four values are booleans
production: yes
debug: off
country: NO # Norway becomes false
answer: y
The NO case is the famous one: a country-code list containing Norway silently
becomes false under a 1.1 parser. There is no error and no warning. The fix is
always the same — quote anything that is semantically a string.
Integers are the second trap. The 1.1 int type supports base-60 (sexagesimal) and leading-zero octal, neither of which exists in the 1.2 core schema:
mode: 0755 # 1.1: octal, decimal 493. 1.2 core: the integer 755
build: 1:30 # 1.1: sexagesimal, 90. 1.2: the string "1:30"
version: 1.20 # a float; 1.20 == 1.2, so the trailing zero is gone
port: "8080" # correct if the consumer wants a string
File modes are the version of this that bites hardest, because
defaultMode: 0755 in a Kubernetes volume looks right and means something
different depending on the parser. Write 0o755 for YAML 1.2 octal, or quote
it and let the consumer parse it.
Timestamps are the third. The
1.1 timestamp type resolves ISO-8601-ish
scalars to date objects, so 2026-07-27 becomes a date, not a string. Round-trip
that through a JSON serializer and you get whatever your language’s default date
format is, which is rarely what you wrote.
Quoting and strings
Three scalar styles, three sets of rules:
- Plain (unquoted): subject to implicit typing, cannot contain
:or#, cannot start with most indicators. - Single-quoted: no escape sequences at all. A literal single quote is
written by doubling it (
'it''s'). Everything else is verbatim. - Double-quoted: the only style with C-style escapes —
\n,\t,\uXXXX. This is where you put control characters.
Block scalars handle multi-line values, and the indicator characters matter:
literal: | # keep newlines, single trailing newline
line one
line two
folded: > # fold newlines into spaces, single trailing newline
this becomes
one long line
strip: |- # keep newlines, strip the trailing one
no trailing newline here
keep: |+ # keep every trailing newline
|- is the one to memorize. Certificates, SSH keys, and tokens embedded in
config almost always need it, because a stray trailing newline changes the
value and produces authentication failures that look like key corruption.
Helm’s YAML techniques guide
covers the same ground from the chart-authoring side.
Structure rules that are not negotiable
Indentation is spaces only. Tabs are forbidden as indentation characters, per
YAML 1.2.2 section 6.1,
and the error message from most parsers is unhelpful, so configure your editor
to expand tabs in .yaml files and stop thinking about it.
A single file can hold multiple documents separated by ---, which is why
kubectl apply -f accepts one file containing a Deployment, a Service, and a
ConfigMap. ... explicitly ends a document and is optional.
Flow style — JSON syntax inside YAML — is legal and often clearer for short structures:
args: ["--verbose", "--dry-run"]
limits: {cpu: 500m, memory: 512Mi}
Note that 500m is a string as far as YAML is concerned; Kubernetes parses
quantity strings itself.
Duplicate keys are invalid per the spec, but parser behavior varies between error, warning, and last-one-wins. Do not rely on any of them. The Kubernetes API server rejects duplicate fields in strict decoding paths, which is the behavior you want everywhere.
Strict decoding is worth enabling deliberately, because the default in a lot of
tooling is to ignore fields it does not recognize. A misspelled
terminationGracePeriodSecond is not an error in a lenient decoder; it is a
field that silently does nothing, and the workload runs with the default
instead of your value. kubectl apply --validate=strict and
kubectl apply --dry-run=server both surface that class of typo before it
reaches a live object, and the server-side variant additionally runs admission
webhooks, so it catches policy rejections at the same time.
Line width is the other structural setting people forget exists. YAML serializers fold long lines by default, and a folded line inside a plain scalar introduces a space where you did not have one. When a serializer emits a base64 blob or a long URL that later fails to parse, check whether it was wrapped before you go looking for an encoding bug.
Anchors, aliases, and merge keys
Anchors let you name a node and reuse it:
defaults: &defaults
restartPolicy: Always
terminationGracePeriodSeconds: 30
web:
<<: *defaults
image: nginx:1.27
&defaults defines the anchor, *defaults references it, and <<: is the
merge key from the 1.1 merge type. Merge
keys are not part of the YAML 1.2 core schema, so support is a parser-by-parser
question — one more reason config that must be portable should be generated
rather than hand-written with clever reuse.
Anchors also carry a denial-of-service risk. A small file with nested aliases
that each reference several others expands exponentially when the parser
materializes it, the “billion laughs” class of resource-exhaustion bug. The
defense is straightforward: parse untrusted YAML with a safe loader
(yaml.safe_load in PyYAML, the default load in js-yaml 4), enforce an input
size limit, and run the parse where a memory ceiling applies. Never use a loader
that can construct arbitrary objects on input you did not write.
JSON: the four rules people get wrong
No comments. There is no comment syntax in RFC 8259 and there never will
be. The convention is a _comment key, or a .jsonc file that you strip
before parsing.
Duplicate names are undefined. The spec says object member names should be unique but does not require it; behavior when they are not is implementation-defined. Most parsers keep the last occurrence. RFC 7493 (I-JSON) tightens this and several other loose ends, and is worth adopting as a house style for APIs.
Numbers are not integers. JSON numbers are decimal literals with no size
limit, but JavaScript parses them into IEEE 754 doubles, so anything above
Number.MAX_SAFE_INTEGER — 9,007,199,254,740,991, per
MDN —
loses precision silently. Snowflake IDs and 64-bit database keys must travel as
strings. RFC 7493 says exactly this.
Encoding is UTF-8. RFC 8259 requires UTF-8 for JSON exchanged between systems that are not part of a closed ecosystem. A byte-order mark is not allowed. If your parser chokes on the first character of a file, check for a BOM before anything else.
Patching, not rewriting
Once a document is JSON, there are two standard ways to describe a change to it
without shipping the whole file, and both show up in Kubernetes tooling.
JSON Patch (RFC 6902) is an
array of explicit operations against JSON Pointer paths, so it can add, remove,
replace, move, and test — and it fails loudly if the document does not look the
way you expected.
JSON Merge Patch (RFC 7396) is
a partial document where present keys replace and null deletes, which reads
better but cannot express “remove the third element of this array”.
# JSON Patch: precise, positional, fails if the path is wrong
kubectl patch deploy/checkout --type=json \
-p '[{"op":"replace","path":"/spec/replicas","value":4}]'
# Merge patch: readable, but null is the only way to delete
kubectl patch deploy/checkout --type=merge \
-p '{"spec":{"replicas":4}}'
Kubernetes adds a third type, strategic merge patch, which understands that
some lists are keyed maps in disguise and merges containers by name instead of
replacing the whole array. That is a Kubernetes extension rather than an IETF
standard, and it is the reason kubectl patch behaves differently from a
generic JSON patch tool on the same object.
What is lost in a YAML to JSON conversion
Converting YAML to JSON is lossy in predictable ways, and it is worth knowing which before you put a converter in a pipeline:
- Comments are gone. They are not part of the data model.
- Anchors and aliases are expanded to their values, so shared structure becomes duplicated structure.
- Multiple documents cannot be represented in one JSON value. You get an array, or an error, or only the first document, depending on the tool.
- Non-string keys — YAML allows any node as a mapping key, JSON requires strings — are stringified or rejected.
- Dates, under a 1.1 parser, become whatever the JSON serializer does with the language-native date object.
Going the other direction, JSON to YAML, is lossless, because JSON is the smaller language. That asymmetry is why config generators emit YAML and validators consume JSON. If you need to check a document against a schema, use JSON Schema against the converted JSON rather than looking for a YAML-native validator.
For quick conversions in both directions, the
YAML to JSON converter on this site runs entirely in the
browser, and the
MDN reference for JSON.parse
documents the reviver hook if you need to intercept values — the usual way to
handle big integers without a custom parser.
The bottom line
The pattern behind every trap in this article is the same: YAML tries to guess what you meant, and the guess is made by whichever parser happens to read the file, not by the person who wrote it. You cannot control the parser in someone else’s CI runner, so the only durable defense is writing documents that have one possible interpretation. That means being explicit even when it looks redundant.
Quote your strings. Assume a 1.1 parser unless you have verified otherwise.
Use |- for keys and certificates. Send 64-bit IDs as strings. Validate with
JSON Schema. Those five habits eliminate most of the category. The
Ansible YAML syntax page
is a good sanity check if you want the same advice from a tool vendor whose
users hit these problems daily.
Frequently asked questions
Is JSON valid YAML?+
Why did my country code NO turn into false?+
country: "NO".How do I write an octal file mode in YAML?+
0o755 for YAML 1.2, or quote it and let the consumer parse it. Bare 0755 is octal under 1.1 and the decimal integer 755 under the 1.2 core schema, which is a real difference in behavior for volume permissions.Can I put comments in JSON?+
Are YAML anchors safe to use in untrusted input?+
Read next
- kubectl cheatsheet (2026 edition)
- Try the tool: YAML to JSON converter
- Try the tool: JSON formatter and validator
Tags: #yaml, #json, #configuration, #kubernetes