What yamldoc does not do¶
A preservation library that is vague about its edges is worse than none, because you find the edge in production. This page is the honest list: things deliberately out of scope, things the design cannot reach, and defects that are known and currently unfixed.
The companion page, What is preserved, covers formatting. This one covers capability.
Deliberately absent¶
These are not gaps to be filled later. They are consequences of the library moving text and assigning it no meaning — the reasoning is in Mechanism, not policy.
| Not provided | Why | What to use instead |
|---|---|---|
| File I/O | Atomic writes, permissions, locking and watching are policy, and callers disagree about all of them | os, afero, your own writer. Parse takes bytes; Bytes returns them |
| Merging, precedence, overlays | Which of two documents wins is an application's semantic, not YAML's | Decide it yourself over Documents() |
| Schema or validation | Whether a value is acceptable depends entirely on the application | Decode into a struct with your usual YAML library |
| Decoding into Go types | This is the editing half of the split; the value half already exists | goccy/go-yaml, go.yaml.in/yaml |
| Creating or deleting documents in a stream | The set of documents is fixed by the source | Concatenate the bytes yourself |
| Formatting or pretty-printing | Reformatting is exactly what this library exists to avoid | yamlfmt, if you want a formatter |
Cannot be addressed, so cannot be edited¶
The path grammar is a dotted list of literal mapping keys, and that ceiling is lower than most people assume. The full account is in Path syntax; the consequences worth knowing before you design around the library:
- No sequence element can be reached. There is no index syntax. To change one item in a list you read the list, rebuild it in Go and set it back — which loses the comments and styles inside that list, because a Go value does not carry them.
- A key containing a dot cannot be reached at all.
log.level: debugat the top level is visible fromKeysand addressable by nothing. - Nothing below an alias can be reached. Walk to the anchor definition instead.
An empty file cannot be filled in¶
Parse accepts empty input, and a comments-only file, and returns a document
that has no root mapping. Every path operation on it then fails with
ErrNotFound:
f, _ := yamldoc.Parse(nil)
err := f.Documents()[0].Set("server.port", 8080)
// yamldoc: path not found: document root is <nil>, not a mapping
The same applies to a document whose root is a sequence or a bare scalar. This
library edits an existing mapping document; it does not author one. If you need
to create a file from nothing, marshal the initial content with an ordinary YAML
library and hand the bytes to Parse afterwards.
A *File is not safe for concurrent use¶
Set and Remove mutate a tree that every *Document from the same *File
shares, and Bytes reads all of it. Nothing in the library locks. Two goroutines
editing the same file will race, and Documents() returning a copied slice does
not change that — the documents inside it are the same objects.
Confine a *File to one goroutine, or guard it with your own mutex. Parsing the
same bytes twice to get two independent files is also fine, and cheap.
Known defects¶
These are real, reproducible and currently unfixed. They are here so you can avoid them, not because they are intended.
Assigning a string that needs quoting¶
Set mutates an existing scalar in place to keep its comment and quoting style.
When the target is a plain, unquoted scalar and the new value is a string that
YAML would need quoted, the text is written literally and no quotes are added.
That output is valid YAML — it just means something else. Re-read, the value is
hello and the rest is a comment. The emit-time guard does not catch it, because
the document parses and decodes perfectly well.
The failures split into two kinds, and the quiet kind is the one to plan for.
Silently wrong — valid YAML, different meaning:
| Value passed | Emitted | Decodes as |
|---|---|---|
"hello # world" |
greeting: hello # world |
hello |
"" |
greeting: |
null |
" leading" |
greeting: leading |
leading |
"123" |
greeting: 123 |
the number 123 |
"null" |
greeting: null |
null |
Loudly refused — the emit guard catches these, Bytes returns
ErrEmitInvalid and no bytes:
"key: value", "- item", "? x", "*alias", "&anchor", "@x", "%x",
"|x", ">x".
Two things bound the blast radius. A target that is already quoted is
re-quoted correctly, escaping and all — 'it''s', "hello # world". And a
newly created key goes through the marshaller, which quotes properly.
It is specifically overwriting a plain scalar that is unsafe.
Until it is fixed, vet strings you did not author: reject or quote a value that is
empty, has leading or trailing whitespace, contains #, or would read as a
number, boolean or null. If you cannot vet it, replace the parent mapping
wholesale so the value goes through the marshaller instead.
A multi-line string over a non-string scalar¶
Assigning a string containing newlines works when the target is already a string — it becomes a literal block. Over an integer, float or boolean target the newlines are written raw:
// given "retries: 1"
doc.Set("retries", "one\ntwo")
out, err := f.Bytes()
// err: yamldoc: refusing to emit invalid YAML: [2:1] non-map value is specified
Nothing is damaged — the emit guard catches this one — but the error arrives as
ErrEmitInvalid, whose own documentation says it indicates a library bug. Here it
does.
A comment on a block that loses its last key is lost¶
The comment ownership rules promise that a comment
describing a block survives the removal of a key inside it, re-homed onto a
neighbour. When the removed key is the block's only key there is no neighbour,
and the mapping collapses to {} — taking the comment with it.
The comment is gone. The same happens to a head comment above the only key. At the
top level the comment does survive ({} # describes the block), because the
collapse is only applied to nested mappings — so whether your comment lives depends
on its depth, which is not a rule anyone would design.
If comments in that position matter to you, read them out with
Node.Comments before removing the key.
A bracketed path silently creates a nonsense key¶
servers[0].port is not an indexed path. It is a path whose first segment is the
literal key servers[0], and Set will happily create one:
No error, and a config file with a key nobody will ever read. If paths come from users, reject bracket syntax before passing it on.
A tag is dropped when its value is replaced¶
A tagged value such as a: !!str 5 round-trips untouched. Assigning over it drops
the tag:
Reading it back is also unusual: Node.String
returns !!str 5, tag included, rather than the value alone.
What is not a limitation, despite appearances¶
Worth stating, because each of these gets reported as a bug.
feature: {}after removing the last key. Deliberate. An empty mapping and an absent key are different states, and code may require the parent to exist. Remove the parent if you want it gone.Removeof an absent key returning an error. Deliberate. Silence there hides a typo in a key name.- Refusing to write through an alias. Deliberate. The library cannot tell whether you mean the shared value or this one use of it.
- Inline comments losing their column alignment. Known and accepted; the reasoning is under Why byte-identity is out of scope.