---
title: "Anchors, aliases and merge keys"
url: https://recursiveai.org/oneyaml/go/cookbook/anchors-aliases-merge-keys/
description: "Read YAML anchors and aliases in Go, turn on << merge keys for docker-compose-style files with MergeKeys(), and follow an alias to its anchor."
version: "1.0.0"
---

# Anchors, aliases and merge keys

Anchors (`&name`) and aliases (`*name`) reuse a node. Merge keys (`<<`) copy a mapping’s entries into another, as docker-compose and many CI files do. YAML 1.2 has no merge keys, so oneyaml reads `<<` as a plain key unless you pass `MergeKeys()`.

```go
package main

import (
	"fmt"
	"log"

	oneyaml "oneyaml.dev/go"
)

const src = `defaults: &defaults
  image: nginx:1.27
  replicas: 2
web:
  <<: *defaults
  replicas: 3
worker: *defaults
`

type Svc struct {
	Image    string `yaml:"image"`
	Replicas int    `yaml:"replicas"`
}

func main() {
	// Aliases are expanded on read; `<<` is a plain key in YAML 1.2.
	var plain map[string]any
	if err := oneyaml.Unmarshal([]byte(src), &plain); err != nil {
		log.Fatal(err)
	}
	fmt.Println("worker:", plain["worker"])
	fmt.Println("web without merge keys:", plain["web"])

	// MergeKeys reads docker-compose-style files the way YAML 1.1 tools do.
	var merged map[string]Svc
	if err := oneyaml.Unmarshal([]byte(src), &merged, oneyaml.MergeKeys()); err != nil {
		log.Fatal(err)
	}
	fmt.Printf("web: %+v\n", merged["web"])

	// A Document keeps anchors and aliases as written, and can follow them.
	doc, err := oneyaml.Parse([]byte(src))
	if err != nil {
		log.Fatal(err)
	}
	worker, _ := doc.Root().Lookup("worker")
	target := worker.Target()
	image, _ := target.Lookup("image")
	fmt.Printf("worker is *%s; &%s has image %s\n", worker.AliasName(), target.Anchor(), image.Value())
}
```

```text
worker: map[image:nginx:1.27 replicas:2]
web without merge keys: map[<<:map[image:nginx:1.27 replicas:2] replicas:3]
web: {Image:nginx:1.27 Replicas:3}
worker is *defaults; &defaults has image nginx:1.27
```

## Good to know

- Keys in the mapping win over merged ones: `web.replicas` is 3, not 2.
- **Alias expansion is bounded by default**, so a “billion laughs” file fails with a `*LimitError` instead of exhausting memory ([read untrusted YAML safely](https://recursiveai.org/oneyaml/go/cookbook/untrusted-input/)).
- **A `Document` keeps `&` and `*` as written.** `AliasName()` names the anchor an alias refers to, `Target()` follows it, and `Anchor()` is a node’s own anchor.
- An edit below an alias is refused with `ErrAliasWrite`, because it would change every place that shares the node. Replacing the alias node itself is allowed. With `MergeKeys`, setting a key that only a `<<` source has adds a key of the mapping’s own.

Merge keys across Go, Rust and C# libraries, measured: [YAML anchors, aliases and merge keys explained](https://recursiveai.org/oneyaml/articles/yaml-anchors-aliases-merge-keys/).
