Why not just re-marshal?¶
The obvious approach to editing YAML in Go is:
var m map[string]any
yaml.Unmarshal(src, &m)
m["server"].(map[string]any)["port"] = 9090
out, _ := yaml.Marshal(m)
It is three lines and it is wrong for any file a human maintains.
What that costs¶
Everything the author wrote that is not data:
- all comments, since
map[string]anyhas nowhere to put them - key order, since Go maps are unordered and marshalling sorts them
- quoting and block styles, since a string is just a string
- anchors and aliases, expanded into duplicated content
A config file with a carefully explained twenty-line preamble comes back with none of it. Worse, it comes back reordered, so the diff is the whole file and nobody can see what actually changed.
Why the obvious libraries do not fix it¶
Reaching for a different YAML library does not help, because the mangling is not a bug in any of them — it is what a value model is for.
gopkg.in/yaml.v3 |
Archived. Rewrites merge keys with explicit tags, reflows folded scalars, escapes astral-plane characters. |
go.yaml.in/yaml/v3 |
The maintained fork, frozen to security fixes. Same behaviour. |
go.yaml.in/yaml/v4 |
Release candidate. Still escapes astral-plane characters unconditionally — and the escaping cascades: a literal block containing an emoji is rewritten as a quoted string. |
braydonk/yaml |
A fork built specifically for preservation. Dormant since early 2025, and abandoned as a dependency by its own primary consumer. |
kyaml |
Pulls in a CLI framework, and re-encodes anyway. |
goccy/go-yaml |
The best parser and CST available, and what this library is built on. It is not an editor: mutation, comment ownership and several corruption cases are left to the caller. |
The pattern every other ecosystem settled on¶
Two libraries, not one:
| Ecosystem | Values | Editing |
|---|---|---|
| Rust | toml |
toml_edit |
| HashiCorp | hclsyntax |
hclwrite |
| Python | tomli |
tomlkit |
Rust is the instructive case. The toml crate was built on toml_edit for two
major versions, then deliberately unwound — parsing Cargo.toml went from 542µs
to 267µs once the value model stopped carrying format-preservation machinery it
did not need.
The lesson is that these are genuinely different jobs. A value model should be fast and lossy; an editing model should be faithful and is allowed to be slower. Trying to be both makes a worse version of each.
yamldoc is the editing half. Keep using your usual library for decoding into
structs — that is what it is good at.
When re-marshalling is the right answer¶
If the file is machine-written and machine-read, re-marshal it. It is simpler and faster, and there is nothing to preserve.
Reach for yamldoc when a human wrote the file, or will read it, or has to
review the diff.