Recursive AI

Getting started with oneyaml for Go

All oneyaml Go docs

Install oneyaml for Go, read YAML into Go structs, write it back, edit a file in place without losing its comments, and apply JSON Patch and merge patches.

oneyaml-go is pure-Go YAML 1.2 that edits a file and keeps its comments. Its Marshal and Unmarshal work the way encoding/json does, and a Document changes only the values you set. Every sample on this page was compiled and run against oneyaml.dev/go v1.0.0; the output under each one is what it printed.

Install

oneyaml needs Go 1.24 or newer.

Terminal
mkdir yaml-start && cd yaml-start
go mod init example.com/yaml-start
go get oneyaml.dev/[email protected]

The import path is oneyaml.dev/go and the package name is oneyaml. It has no dependencies outside the standard library, no unsafe and no cgo. The license is MIT or Apache-2.0, at your choice.

The samples are complete main.go files. A few read a file from the current directory; the YAML they read is shown with them.

Read YAML into a struct

oneyaml.Unmarshal fills a struct by its yaml tags, the way encoding/json does with json tags.

GO
package main
 
import (
	"fmt"
	"log"
 
	oneyaml "oneyaml.dev/go"
)
 
type Config struct {
	Name     string `yaml:"name"`
	Replicas int    `yaml:"replicas"`
	Ports    []int  `yaml:"ports"`
	Debug    bool   `yaml:"debug"`
}
 
func main() {
	src := []byte(`name: api
replicas: 2
ports: [80, 443]
debug: false
`)
	var cfg Config
	if err := oneyaml.Unmarshal(src, &cfg); err != nil {
		log.Fatal(err)
	}
	fmt.Printf("%+v\n", cfg)
}
Text
{Name:api Replicas:2 Ports:[80 443] Debug:false}

YAML 1.2 is the default: plain yes is a string, plain 0755 is the integer 755, and quoted "0755" is a string. A value that doesn’t fit its field is a *TypeError with a path, a line and a column, and the rest of the document is still read (see errors with line and column).

Write YAML

oneyaml.Marshal writes any Go value as block-style YAML.

GO
package main
 
import (
	"fmt"
	"log"
 
	oneyaml "oneyaml.dev/go"
)
 
type Config struct {
	Name     string            `yaml:"name"`
	Replicas int               `yaml:"replicas"`
	Ports    []int             `yaml:"ports"`
	Labels   map[string]string `yaml:"labels,omitempty"`
}
 
func main() {
	cfg := Config{
		Name:     "api",
		Replicas: 3,
		Ports:    []int{80, 443},
		Labels:   map[string]string{"tier": "web"},
	}
	out, err := oneyaml.Marshal(cfg)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Print(string(out))
}
Text
name: api
replicas: 3
ports:
  - 80
  - 443
labels:
  tier: web

NewEncoder writes to an io.Writer, one document per Encode, and Indent(n) sets the indent.

Edit a file and keep its comments

To change a file that people also edit by hand, parse it into a Document instead. Set rewrites one value in its own style; comments and formatting elsewhere are kept byte for byte.

GO
package main
 
import (
	"fmt"
	"log"
 
	oneyaml "oneyaml.dev/go"
)
 
func main() {
	src := []byte(`# Deploy settings
name: api
replicas: 2   # raised by the release job
ports: [80]
`)
	doc, err := oneyaml.Parse(src)
	if err != nil {
		log.Fatal(err)
	}
 
	// Read a value by JSON Pointer.
	var replicas int
	if err := doc.Get("/replicas", &replicas); err != nil {
		log.Fatal(err)
	}
	fmt.Println("replicas =", replicas)
 
	// Change a value, append to a list, add a key.
	for _, step := range []struct {
		ptr string
		v   any
	}{
		{"/replicas", 3},
		{"/ports/-", 443},
		{"/region", "us-east-1"},
	} {
		if err := doc.Set(step.ptr, step.v); err != nil {
			log.Fatal(err)
		}
	}
	fmt.Print(doc)
}
Text
replicas = 2
# Deploy settings
name: api
replicas: 3   # raised by the release job
ports: [80, 443]
region: us-east-1

Paths are JSON Pointers (RFC 6901): /replicas, /spec/containers/0/image, and /ports/- for the end of a list. A value that is replaced keeps its style when it can hold the new value; a new member copies the style of its siblings, so the flow list [80] stays a flow list. Remove, Rename, SetHeadComment and SetLineComment do the rest (Editing).

Apply a JSON Patch or a merge patch

A Document applies RFC 6902 JSON Patch and RFC 7396 merge patches. A patch applies atomically and keeps the file’s layout.

GO
package main
 
import (
	"fmt"
	"log"
 
	oneyaml "oneyaml.dev/go"
)
 
const src = `# Web service
image: web:1.0   # pinned
env:
  DEBUG: "1"
  LOG: info
ports: [80]
`
 
func main() {
	// JSON Patch (RFC 6902): all or nothing.
	doc, err := oneyaml.Parse([]byte(src))
	if err != nil {
		log.Fatal(err)
	}
	err = doc.ApplyPatch([]byte(`[
		{"op": "replace", "path": "/image", "value": "web:1.1"},
		{"op": "add", "path": "/ports/-", "value": 443}
	]`))
	if err != nil {
		log.Fatal(err)
	}
	fmt.Print(doc)
	fmt.Println("---")
 
	// Merge patch (RFC 7396): null removes a key.
	doc, err = oneyaml.Parse([]byte(src))
	if err != nil {
		log.Fatal(err)
	}
	merge := []byte(`{"env": {"DEBUG": null, "LOG": "warn"}}`)
	if err := doc.ApplyMergePatch(merge); err != nil {
		log.Fatal(err)
	}
	fmt.Print(doc)
}
Text
# Web service
image: web:1.1   # pinned
env:
  DEBUG: "1"
  LOG: info
ports: [80, 443]
---
# Web service
image: web:1.0   # pinned
env:
  LOG: warn
ports: [80]

Patches use YAML’s data model: test compares YAML values, so the integer 1 and the float 1.0 differ, and a write through an alias is refused. Apply takes the same operations built in Go with PatchTest, PatchReplace and the others (Patches).

Where next

Recipes, each a complete program:

The docs:

Choosing a Go YAML library, with four libraries compared on the same file: Golang YAML.