YAML Cheatsheet

Merge Keys

Use this YAML reference while you build software engineering projects, review code, or refresh the syntax you reach for most.

What << does

<<: *base copies every key from the aliased mapping into the current mapping. Unlike a bare alias, the result is a new mapping you can add to and override.

defaults: &defaults
  adapter: postgres
  host: localhost
  port: 5432

dev:
  <<: *defaults
  database: dev_db

prod:
  <<: *defaults
  host: db.internal
  database: prod_db

parses to:

{"defaults": {"adapter": "postgres", "host": "localhost", "port": 5432},
 "dev":  {"adapter": "postgres", "host": "localhost",
          "port": 5432, "database": "dev_db"},
 "prod": {"adapter": "postgres", "host": "db.internal",
          "port": 5432, "database": "prod_db"}}

prod.host is db.internal. An explicit key always wins over a merged one, regardless of whether it appears before or after the <<: line.

Precedence

Three levels, highest first:

  1. Keys written explicitly in this mapping.
  2. Keys from the first alias in a multi-merge list.
  3. Keys from later aliases in that list.
a: &a {x: 1, y: 1}
b: &b {y: 2, z: 2}

merged:
  <<: [*a, *b]
  z: 99

parses to {"merged": {"y": 1, "z": 99, "x": 1}}. Read that carefully:

  • x comes from *a only.
  • y is 1, from *a, because *a is earlier in the list and earlier wins.
  • z is 99, the explicit key, beating *b.

This is the opposite of the CSS or Object.assign intuition, where later wins. In a merge-key list, earlier wins.

The forms

FormMeaning
<<: *basemerge one mapping
<<: [*a, *b]merge several, earlier takes precedence
<<: {x: 1}merge an inline mapping, no anchor needed
<<: *a twice in one mappinga duplicate key, parser dependent

Only mappings can be merged. <<: *some_list is an error, and there is no merge for sequences at all. Concatenating two lists is not something YAML can do.

Merging is shallow

<< copies top-level keys. A nested mapping is copied by reference, so it is not merged recursively.

base: &base
  db:
    host: localhost
    port: 5432

override:
  <<: *base
  db:
    host: prod.internal      # port 5432 is GONE

override.db is {"host": "prod.internal"}. The whole db mapping was replaced, not deep-merged. Anchor the inner mapping too if you need that:

x-db: &db
  host: localhost
  port: 5432

override:
  db:
    <<: *db
    host: prod.internal      # {"host": "prod.internal", "port": 5432}

Support is not universal

Merge keys were a YAML 1.1 type, defined at tag:yaml.org,2002:merge. They were not carried into the YAML 1.2 core schema, so a strict 1.2 parser treats << as an ordinary string key.

Parser<<: support
PyYAML safe_loadyes
ruamel.yaml (1.1 and 1.2 modes)yes
Go gopkg.in/yaml.v3yes
Ruby Psychyes
js-yamlyes (merge is in the default schema)
yaml (Node, eemeli)opt-in, parse(src, {merge: true})
strict 1.2 / JSON-schema-only parsersno, << is a literal key

The failure mode when it is unsupported is quiet: you get a key literally named << whose value is a copied mapping, and every field you expected to be inherited is missing.

Tool support in practice

ToolMerge keys
Docker Composeyes
GitLab CIyes, though extends: is preferred and deep-merges
Ansibleyes
Kubernetes manifestsyes, resolved before the API server sees them
CircleCIyes, the standard way to share job config
GitHub Actionsno, aliases are rejected entirely

GitLab's own extends: keyword exists because it deep-merges where << is shallow. When both are available, prefer the tool's native mechanism.

Debugging

yq resolves merges and aliases so you can see the flattened result:

yq 'explode(.)' docker-compose.yml
yq '.prod' config.yml            # merged view of one key
python -c "import yaml,sys,json; \
  print(json.dumps(yaml.safe_load(open('config.yml')), indent=2, default=str))"

If explode shows a literal << key in the output, the file is being read by something that does not support merges, and you need to flatten it by hand or switch parsers.