YAML and .properties, Properly Explained
YAML 1.0 was published in 2001 by Clark Evans, Ingy döt Net, and Oren Ben-Kiki, designed to be 'human-friendly' where XML was machine-friendly and JSON didn't yet exist as a separate format. The vision was beautiful: a config file that looks like prose. The reality is a format whose 1.1 spec contains a coercion table responsible for more outages than any single feature in any other config format, and whose 1.2 spec fixes most of them — except your tooling probably still implements 1.1.
YAML and Java's .properties format both solve the same problem — configuration as text — and they took opposite paths. YAML maximized expressivity (nested structures, anchors, multi-line strings, tagged types). .properties maximized boredom (one key, one value, one line). The world has spent twenty years discovering that the boring one was right more often than anyone wanted to admit.
YAML in one paragraph
YAML is a superset of JSON. Every valid JSON document is valid YAML 1.2. On top of JSON's grammar, YAML adds: indentation-based nesting, unquoted strings, multi-line strings, comments (#), references via anchors and aliases (&id / *id), explicit type tags (!!str, !!binary), and a parade of implicit type coercions that converted strings into numbers, booleans, and dates without you asking.
The format reads like a stripped-down config DSL. Empty config files are valid. A single string is a valid document. Lists, maps, and scalars compose like Lego.
service: api-gateway
replicas: 3
flags:
- logging
- tracing
env:
LOG_LEVEL: info
TIMEOUT: 30s
That's the surface YAML. The trouble is everything underneath.
The Norway Problem
This is the bug that defined YAML 1.1's reputation.
countries:
- GB
- IE
- NO
- SE
In YAML 1.1, that document parses as:
{'countries': ['GB', 'IE', False, 'SE']}
NO is not a country code in YAML 1.1; it's a boolean false. So is No, no, N, n, false, False, FALSE, off, Off, OFF. The boolean-coercion table accepted a comically wide range of inputs. Norway's two-letter country code happened to be one of them. Stack Overflow questions, library bug reports, and at least one widely-reported deployment incident all resolved to the same root cause: YAML 1.1 thought NO was a boolean.
YAML 1.2, published in 2009, fixed this — booleans are now strictly true and false. But here's the catch: many parsers in the wild still default to YAML 1.1 semantics, including PyYAML's default loader for years, and several Ruby libraries, and the YAML in some build systems. The fix is to use a 1.2-strict loader (PyYAML's safe_load plus Loader=yaml.CSafeLoader, or ruamel.yaml), but you have to know to ask.
The other implicit-coercion footguns
The Norway Problem is only the most famous. YAML 1.1 also coerced:
12:34:56as a sexagesimal number:12*3600 + 34*60 + 56 = 45296. Time-of-day strings became integers.0123as octal83. Anything starting with a leading zero might be octal.1.0as a float,1.0e2as 100.0. Version strings written as1.0became floats and lost trailing-zero formatting.yes,no,on,offas booleans. Yes-or-no answers became truthy/falsy in unpredictable cases.null,Null,NULL,~, (empty) all as null.2010-04-01as a date object. Version numbers that happen to look like dates became dates.
The pattern: any value that looks like a special type is that type, unless quoted. The rule for safety is therefore: quote anything that could plausibly be misinterpreted. version: "1.0", code: "NO", time: "12:34:56", port: "0123". The cost is mild visual noise; the benefit is no surprise type coercions.
YAML 1.2 narrowed the implicit-type set significantly: only true/false, null/~/empty, integers, and floats are coerced. NO, yes, 12:34:56, 2010-04-01 are all strings. But again — if your parser is in 1.1 mode, this doesn't help you.
Indentation: significant, in the worst way
Python's significant indentation is mostly fine because Python's parser tells you immediately when indentation is wrong. YAML's significant indentation is treacherous because YAML accepts many indentation choices as valid but with different meanings.
items:
- name: alpha
qty: 3
- name: beta
qty: 5
vs.
items:
- name: alpha
qty: 3
- name: beta
qty: 5
The second version is also a valid YAML document, but qty is now a sibling of items, not a property of each item. The parser doesn't tell you you got it wrong; it just gives you a document with a different shape, and your application explodes downstream.
Tabs are also banned for indentation in YAML, and most editors that auto-convert tabs to spaces will silently fix this — or, occasionally, silently mix them, in which case the parser bails with a cryptic error.
The pragmatic rule: lint your YAML with a real linter (yamllint, kubeval for Kubernetes specifically) before deploying. The format gives you no compile-time check; you have to bolt one on.
Anchors and aliases: the feature you'll regret using
YAML lets you reference a node from elsewhere in the document:
defaults: &defaults
timeout: 30
retries: 3
services:
api:
<<: *defaults
port: 8080
worker:
<<: *defaults
port: 9000
&defaults declares an anchor; *defaults references it; <<: merges the anchored map into the current map. It looks DRY, it scales reasonably, and it fails in three ways:
- It composes with multi-document streams oddly. Anchors are per-document; carrying them across documents requires extension support.
- It produces output that's not safe to round-trip through arbitrary tools. Many tools resolve aliases on read, so the dumped output loses the structure.
- It enables denial-of-service attacks when an attacker can author the YAML — the YAML equivalent of XML's billion laughs:
a: &a [1,1,1,1,1,1,1,1,1]
b: &b [*a,*a,*a,*a,*a,*a,*a,*a,*a]
c: &c [*b,*b,*b,*b,*b,*b,*b,*b,*b]
# ... by g, you've allocated 10^9 ints
Most modern parsers cap alias expansion. Don't disable that cap. Don't accept anchored YAML from untrusted sources unless your parser explicitly safe-handles it.
Multi-document streams
A single YAML file can contain multiple documents separated by ---:
---
kind: Deployment
metadata:
name: api
---
kind: Service
metadata:
name: api-svc
This is heavily used by Kubernetes and Helm. The format is part of the YAML spec; many YAML libraries return a sequence (Python yaml.safe_load_all, Go yaml.NewDecoder().Decode in a loop), and the optional trailing ... end-of-document marker is rarely seen but valid.
Multi-line strings: more flavors than you want
YAML has five ways to write a string spanning multiple lines, and they differ in whitespace handling:
literal: | # preserve newlines
line one
line two
folded: > # collapse newlines into spaces
line one
line two
literal-strip: |- # preserve newlines, strip trailing
line one
literal-keep: |+ # preserve newlines, keep all trailing
line one
double-quoted: "line one\nline two\n" # JSON-style escapes work
The | literal block keeps newlines; the > folded block joins them with spaces (except blank lines, which become real newlines). The - chomping indicator strips trailing newlines, + keeps all of them, default keeps exactly one. Get the chomping wrong and your shell-script-in-YAML produces an extra newline that breaks the script.
.properties: the boring format that wouldn't die
Java's .properties format predates the millennium and has barely changed. The grammar:
key=value
key.with.dots=another value
key.with.spaces = padding around the equals is okay
multi.line=one line\
second line continued
empty.value=
# comment line
! also a comment line
key:value # colon also separates
That's most of it. Keys are strings; values are strings; nesting doesn't exist; you fake hierarchy with dotted keys. Encoding is officially ISO-8859-1, with \uXXXX escapes for anything else (Java Properties files are a fossilized window into pre-UTF-8 i18n).
Why it survived: it's impossible to misparse. There is no implicit coercion. There is no boolean Norway. There are no anchors. There is no indentation to get wrong. The parser is twenty lines of code in any language. Spring, Java EE, Maven, Gradle, log4j, Kafka, Hadoop, JBoss — the entire JVM ecosystem standardized on it because it was simple and stayed simple.
The downsides: no native nesting, no native lists, no comments-on-the-same-line-as-a-value, ISO-8859-1 by default. Modern Spring and Spring Boot accept both .properties and YAML; most teams pick YAML for new code and live with .properties for legacy.
Converting between them
Conceptually:
a.b.c=valuein.properties→{a: {b: {c: value}}}in YAML.a.b[0]=x(Spring's list extension) →{a: {b: [x]}}.- A YAML map flattens to dotted keys; a YAML list flattens to indexed keys (
a[0],a[1]).
Two pitfalls:
- Type loss in either direction. YAML's
enabled: trueis the boolean true. The.propertiesequivalent isenabled=true, which is the string"true". The consumer has to know to coerce. .propertiesallows keys with dots that aren't structural.web.url=http://...could mean "a key called web.url" or "the url child of web." YAML conversion has to make a choice; the right choice is application-specific.
The conversion is mostly mechanical, and the in-browser tool here implements it both ways with the standard flattening rules. It won't recover information that wasn't expressible in the source — there's no way for .properties to encode a YAML document with anchor references — but for the common case of flat-config-vs-nested-config, the round-trip is clean.
Common pitfalls
- YAML 1.1 booleans treating
NO,Yes,Offas booleans. Quote them or upgrade your parser. - Leading zeros read as octal in 1.1. Quote phone numbers, ZIP codes, account numbers — anything that's a string of digits but not arithmetic.
12:34read as sexagesimal. Quote times.- Tab vs space indentation. YAML rejects tabs.
- Wrong indentation level parses as a different document shape, silently. Lint.
- Unquoted dates become date objects, not strings. Version strings like
2024-06are at risk. null,~, empty value all mean null. An emptykey:is null, not an empty string. Usekey: ""if you want empty.- Multi-line block chomping producing an extra newline you didn't want. Use
|-if your downstream consumer is whitespace-sensitive. .propertiesline continuations with\at end of line require leading-whitespace stripping on the next line; some hand-rolled parsers get this wrong..propertiesUnicode when you forgot to\uXXXX-escape non-ASCII characters in an ISO-8859-1 file. Modern Spring accepts UTF-8 if you tell it; check your loader.
When to use which
- YAML for: Kubernetes manifests, GitHub Actions workflows, Ansible playbooks, anywhere the existing ecosystem expects YAML, anywhere humans need to author multi-level config.
- .properties for: Spring config, Java application properties, anywhere the consumer is JVM-side and the structure is flat-ish.
- TOML when you have the freedom to choose. It's what YAML wishes it had been: human-readable, comments, no implicit type coercion, no significant indentation. Used by Cargo, pyproject.toml, many newer tools.
- JSON for everything machine-to-machine, including config files generated by tools.
- Environment variables for the smallest, flattest, most transport-agnostic configuration. Twelve-factor was right about this.
If you can choose, choose TOML for new human-authored configs. If the ecosystem made the choice for you (Kubernetes, GitHub Actions, Spring), use what it expects, quote your strings defensively, lint your files in CI, and never trust YAML 1.1 with a country code.
Convert between YAML and .properties
The YAML / .properties tool on this site converts in both directions, in the browser, with structure-preserving flattening. Useful when migrating between Spring config and Kubernetes / GitHub Actions YAML. Nothing leaves your browser.
Open the YAML toolRelated guides
Keep the session useful with adjacent reading instead of exiting after one article.
QR Codes, Properly Explained
How QR codes actually work — finder patterns, Reed-Solomon error correction, static vs. dynamic redirects, and the real reasons codes fail in print.
Base64, Properly Explained
A 1989 hack for smuggling binary through 7-bit email transports — and why we still use it for JWTs, data URIs, and a hundred other places. Two alphabets, one common decode failure, and the things it categorically isn't.
URL Encoding, Properly Explained
Why %20 and + both mean space, why encodeURI and encodeURIComponent are not interchangeable, and how the HTML form spec quietly invented its own incompatible variant. RFC 3986 vs application/x-www-form-urlencoded.