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.
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.
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) }
{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.
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)) }
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.
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) }
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.
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) }
# 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:
- Parse YAML into Go structs and read YAML without a struct
- Edit YAML and keep its comments and merge or patch YAML
- Convert YAML to JSON and back and read Kubernetes manifests and multi-document files
- Report errors with line and column, anchors, aliases and merge keys
- Read untrusted YAML safely and switch from yaml.v3 with one import
The docs:
- Editing, patches and limits. The API reference with a runnable example for every feature is on pkg.go.dev.
- Moving from another library: yaml.v3 and sigs.k8s.io/yaml.
- Choosing a library: how it compares, performance and conformance.
- What changed and what stays stable: the changelog and stability.
Choosing a Go YAML library, with four libraries compared on the same file: Golang YAML.