Recursive AI

Anchors, aliases and merge keys

All oneyaml Go recipes

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).
  • 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.