Andrew Mercer

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, n are booleans under YAML 1.1 but not YAML 1.2. This is the single most common real-world YAML bug: a top-level on: key in a GitHub Actions workflow gets parsed as boolean true by some YAML 1.1-based linters/tools. Quote it when in doubt: "on":.
  • The "Norway problem": the unquoted country code NO becomes boolean false under YAML 1.1 rules — a classic gotcha for anyone hardcoding country codes in YAML data.
  • Leading zeros can trigger octal interpretation in some parsers (0755 might 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)jq for 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