Comprehensive Guide to YAML¶
What It Is¶
YAML ("YAML Ain't Markup Language") is a human-readable data serialization format used heavily for config files, CI/CD pipelines (GitLab CI, GitHub Actions), Kubernetes manifests, Ansible playbooks, Docker Compose, and Helm charts. It's a superset of JSON — any valid JSON is technically valid YAML — but adds comments, anchors, multi-line strings, and cleaner human-editable syntax on top.
The official spec lives at yaml.org. Current versions in wide use are YAML 1.1 and 1.2 — most modern parsers (PyYAML with safe_load, Go's yaml.v3) lean toward 1.1 behavior in places, which matters for the boolean gotchas below.
Core Syntax¶
Whitespace is structural — indentation defines nesting (spaces only, never tabs). Comments start with # and run to end of line.
Scalars (key-value pairs)¶
name: Andrew
role: Platform Engineer
active: true
count: 42
ratio: 3.14
nothing: null # or ~, or leave the value blank
Maps (dictionaries)¶
Nest via indentation — consistent indent width (commonly 2 spaces) throughout the file:
server:
host: localhost
port: 8080
tls:
enabled: true
cert_path: /etc/certs/server.pem
Sequences (lists)¶
Block style:
tools:
- terraform
- kubernetes
- elasticsearch
Flow style (inline, JSON-like):
tools: [terraform, kubernetes, elasticsearch]
Lists of maps:
servers:
- name: web1
ip: 10.0.0.1
- name: web2
ip: 10.0.0.2
Multi-line strings¶
literal: |
This preserves
line breaks exactly,
including this indentation.
folded: >
This folds
newlines into spaces,
producing one long line.
Chomping indicators control trailing newlines:
- |- / >- — strip the trailing newline entirely
- | / > — keep a single trailing newline (default)
- |+ / >+ — keep all trailing blank lines
Quoting¶
Three styles: unquoted (plain), single-quoted ('...', no escape sequences — '' represents a literal single quote), and double-quoted ("...", supports backslash escapes like \n, \t).
Quote a value whenever it could be misread as another type:
zip_code: "07030" # without quotes, could be read oddly or lose a leading zero as a number
enabled: "yes" # without quotes, YAML 1.1 parsers read this as boolean true
version: "1.20" # without quotes, could be parsed as a float and normalize to 1.2
Data Types¶
YAML's core schema defines these standard tags:
| Tag | Meaning |
|---|---|
!!seq |
Sequence (list) |
!!map |
Map (dictionary) |
!!str |
String |
!!int |
Integer |
!!float |
Floating-point decimal |
!!null |
Null |
!!binary |
Binary data (base64-encoded) |
!!omap |
Ordered map (preserves insertion order explicitly) |
!!set |
Unordered set |
Forcing a type without quotes¶
version: !!str 13 # forces "13" to be treated as a string, not integer 13
port: !!int "8080" # forces a string-looking value to be parsed as an integer
This is handy in templated YAML or generated config where wrapping in quotes is awkward, but you still need to pin the resulting type explicitly.
Custom Tags¶
%TAG ! tag:hostdata:your_tag:
---
record: !YOUR_TAG
id: 1001
region: us-east
A %TAG directive at the top of a document (before the ---) remaps the default ! handle to a custom URI prefix. After declaring it, nodes tagged with !YOUR_TAG carry that semantic type. This is metadata for tools that understand it — a schema-aware loader, a custom deserializer, Ansible's own tag extensions — and plain parsers simply pass it through or ignore it if they don't recognize it.
Anchors and Aliases (DRY YAML)¶
Anchors let you define a chunk once and reuse it elsewhere in the same document:
defaults: &defaults
adapter: postgres
host: localhost
pool: 5
development:
<<: *defaults
database: dev_db
test:
<<: *defaults
database: test_db
pool: 2 # overrides the merged value
&anchor_name— defines the anchor at that node*anchor_name— references (aliases) it elsewhere, producing an identical copy<<:— the "merge key" — merges an anchored map's keys into the current map; keys defined after<<:locally override the merged ones
This pattern is extremely common in .gitlab-ci.yml for shared job templates, and in Docker Compose for shared service config.
Multiple Documents in One File¶
---
doc: 1
---
doc: 2
...
--- starts a new document within the same file/stream; ... optionally marks the explicit end of one. Kubernetes manifests routinely stack multiple resources (a Deployment and a Service, say) in one file this way.
Gotchas¶
- Tabs are illegal for indentation — most parsers will hard-fail on a tab character in leading whitespace.
on,off,yes,no,y,nare booleans under YAML 1.1 but not YAML 1.2. This is the single most common real-world YAML bug: a top-levelon:key in a GitHub Actions workflow gets parsed as booleantrueby some YAML 1.1-based linters/tools. Quote it when in doubt:"on":.- The "Norway problem": the unquoted country code
NObecomes booleanfalseunder YAML 1.1 rules — a classic gotcha for anyone hardcoding country codes in YAML data. - Leading zeros can trigger octal interpretation in some parsers (
0755might not mean the number 755). - Trailing whitespace after a
:before a value, or inconsistent indentation between sibling keys, causes subtle parse errors that don't always produce clear error messages. - Duplicate keys are technically an error per spec, but many parsers silently accept them and just take the last value — don't rely on this being caught for you.
Tools¶
- yamllint.com — quick online linter/validator for one-off checks
- yq (mikefarah/yq) —
jqfor YAML; a single Go binary for reading, writing, and transforming YAML from the CLI:
bash yq '.spec.replicas' deployment.yaml yq -i '.metadata.labels.env = "prod"' deployment.yaml yq -o json eval file.yaml # convert YAML to JSON yq -p json -o yaml eval file.json # convert JSON to YAML yamllint(the Python package, distinct from the website) — the standard for CI/pre-commit style and syntax checking, with configurable rule sets- Most CI systems (GitLab, GitHub Actions) ship their own schema validators that catch YAML errors specific to that platform's expected structure, worth running alongside a general linter