Config Formats

YAML Gotchas: Type Coercion, Indentation and Multiline Strings

YAML files that "look right but run wrong" usually fail in three places: silently coerced scalars, misplaced indentation, and ambiguous multiline strings. This guide dissects each trap with detection methods and a toolchain.

By LaoHand Team·8 min read·Updated 2026-08-31

Type coercion: the biggest silent bomb

YAML infers scalar types automatically, and many values that "look like strings" get silently converted: no/na/n become the boolean false (the famous Norway problem), on/off/yes become true/false, 0733 becomes octal, and 1.10 becomes the number 1.1.

The result is config that "looks right but runs wrong": version numbers lose precision, country codes turn into booleans. Unless you explicitly want a number/boolean/null, quote every scalar — the single most stress-reducing YAML rule.

country: no          # Actually false, not Norway!
country: "no"       # This is a string
version: 1.10       # Becomes the number 1.1
version: "1.10"    # String, safe

When quoting is mandatory

Three classes of values must be quoted: (1) anything that could be coerced to a number/boolean/null (no, on, 1.10, plus 2026-01-01, which parses as a date object); (2) values starting with special characters (* & ! % @); (3) values containing "colon + space" or #.

The date trap deserves emphasis: date: 2026-01-01 is a date object in most parsers, not a string, and it surfaces in a totally different shape during log debugging. Write date: "2026-01-01" for a string.

date: 2026-01-01      # Date object, not a string
date: "2026-01-01"   # String
value: a: b           # Syntax error (colon + space)
value: "a: b"        # Correct

Indentation: one level off, data lands elsewhere

YAML accepts spaces only, never tabs (a tab is a hard syntax error); sibling keys must align; content after a list marker - belongs to a child level. One level off and data silently lands under the wrong node — most parsers never complain.

Two frequent details: a space must follow the colon (key:value is invalid), and a comment # needs a preceding space or it becomes part of the value. A YAML language server (e.g. Red Hat YAML for VSCode) flags these live.

server:
  host: 0.0.0.0     # Indented 2 spaces, belongs to server
  ports:
    - 80            # List items indent one more level
    - 443
# key:value is wrong → key: value is right

Multiline strings: | vs >

The pipe | keeps newlines (literal block); the fold > collapses newlines into spaces (folded block); adding - strips the trailing newline (|-, >-). Use | for scripts, certificates and SQL; use > for long prose.

The indentation baseline is set by the first line: every line in the block must be indented deeper than the parent key and consistently — this is exactly why "it looks aligned but still errors": you compared against the wrong level.

script: |
  #!/usr/bin/env bash
  set -euo pipefail
  echo "ok"
desc: >
  This is a long block of description text,
  where newlines are folded into spaces.
sql: |-
  SELECT *
  FROM users;

Anchors, aliases and merge keys

An anchor &name marks a node, an alias *name references it, and <<: merges a mapping into the current key — this is how docker-compose reuses shared config.

Two limits to remember: lists cannot be merged with <<, only referenced whole; an alias is a reference, not a copy, so changing one place changes all. Bonus trap: unquoted 80:80 parses as a sexagesimal (base-60) number under YAML 1.1 — always quote port mappings as "80:80" in compose files.

common: &common
  image: nginx:1.27
  restart: unless-stopped
web:
  <<: *common
  ports:
    - "80:80"

Debugging toolchain and checklist

Validate before shipping: yamllint for style and structure, the consuming tool itself (docker compose config, kubectl --dry-run), or a one-liner yaml.safe_load in Python to confirm what the parser actually sees.

Debugging principle: the reported line usually marks where the problem surfaced, while the real mistake sits a few lines above in indentation or quoting. Checklist: quote scalars, spaces only, space after the colon, pick the right |/> block, quote port mappings, run a parser before committing.

yamllint config.yaml
docker compose config        # Parse and output normalized config
kubectl apply --dry-run=client -f manifest.yaml
python -c "import yaml,sys; yaml.safe_load(open(sys.argv[1]))" config.yaml

Official References

Each command links to its official documentation below, so you can verify the latest usage and read deeper.