Skip to content

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]any has 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.