Skip to content

API reference

Every exported symbol in gitlab.com/phpboyscout/go/yamldoc, what it returns, and what happens when the input is wrong.

Import path and version:

import "gitlab.com/phpboyscout/go/yamldoc"

Go 1.26 or later. One dependency, github.com/goccy/go-yaml.

The generated godoc is on pkg.go.dev; this page adds the behaviour that a signature does not show.

Parse

func Parse(src []byte) (*File, error)

Reads a YAML stream and returns the file it describes. src may hold several documents separated by ---.

Parsing never fails because a document contains something that cannot be round-tripped safely — those are reported by File.Unsupported and left for you to judge. It fails when the bytes are not YAML the parser accepts:

Input Result
Well-formed YAML *File, nil
Tab-indented YAML error: found character '\t' that cannot start any token
A duplicate mapping key error: mapping key "a" already defined at [1:1]
Empty input a *File with one document that has no root mapping
A comments-only file a *File with one document whose body is the comment group

The two empty-ish cases parse successfully and then refuse every path operation with ErrNotFound, because there is no mapping to address into. yamldoc edits documents; it does not create them from nothing.

Duplicate keys are worth calling out because the source says otherwise: the ReasonUndecodable doc comment lists a duplicate mapping key as something reported through Unsupported. With goccy/go-yaml v1.19.2 the parser rejects it first, so Parse returns an error and you never see an Unsupported entry for it.

File

A parsed YAML stream. Not safe for concurrent use — Set and Remove mutate a tree that every *Document from the same *File shares, and nothing in the library locks. Confine a *File to one goroutine, or guard it yourself.

File.Documents

func (f *File) Documents() []*Document

The file's documents, in the order they appear in the source.

The slice is a copy, so reordering or truncating it changes nothing; the *Document values inside it are shared, which is what makes editing one visible in Bytes. There is no way to add or delete a document — the set is fixed by the source.

The library assigns no meaning to document order. Treating later documents as overriding earlier ones is a caller's convention; see Mechanism, not policy.

File.Unsupported

func (f *File) Unsupported() []Unsupported

Constructs found in the source that cannot be safely round-tripped. Empty means the file can be emitted.

The slice is a copy. See Unsupported constructs for what lands here and why.

File.Bytes

func (f *File) Bytes() ([]byte, error)

Re-emits the whole file, all documents.

On any error the returned slice is nil — this call never hands back damaged bytes. Three outcomes:

Condition Returns
Nothing unsupported, output re-parses and decodes the bytes, nil
Unsupported() is non-empty nil, ErrUnsupported wrapping the first report
Output does not parse, or parses but does not decode nil, ErrEmitInvalid

The second check is the one that is easy to miss: the output is fully decoded, not just parsed, because a dangling alias is invisible to a parser and only surfaces when aliases are resolved.

Emission converges immediately. Emitting a file, re-parsing it and emitting again produces identical bytes, so a tool that writes on every keystroke does not accumulate drift.

Document

One document within a *File. All four methods take a dotted path; see Path syntax for what a path can and cannot address.

Document.Index

func (d *Document) Index() int

The document's position in the file, starting at zero.

Document.Get

func (d *Document) Get(path string) (Node, bool)

The node at path, read-only. ok is false — with no error and no distinction between the cases — when:

  • the path is malformed (empty, or with an empty segment);
  • the document has no root mapping (an empty file, or a root that is a sequence or a scalar);
  • any segment does not exist;
  • a segment before the last one is not a mapping.

A Node points into the parsed document rather than copying out of it. A Node held across a later Set on the same path reads the new value; one held across a Remove keeps reading the value it had, now detached from the document and about to be garbage. Fetch the node when you need it rather than caching it across edits.

Document.Keys

func (d *Document) Keys(path string) ([]string, bool)

The immediate child keys of the mapping at path, in document order. An empty path returns the document's top-level keys.

ok is false when the path does not resolve, or resolves to something that is not a mapping (a scalar, a sequence, an alias).

Two things the list includes that callers tend not to expect:

  • A merge key is a key. A mapping written <<: *base reports << alongside its ordinary keys.
  • Keys that no path can address. A key containing a dot is listed verbatim as a.b, and there is no escape that lets Get, Set or Remove reach it.

Keys whose value is an empty collection are included: emptiness is a value.

Document.Set

func (d *Document) Set(path string, value any) error

Assigns value at path, creating the key and any missing intermediate mappings.

value is anything yaml.Marshal accepts — a scalar of any width, a string, bool, nil, a map, a slice, or a struct with yaml tags. Scalars replacing scalars are mutated in place; everything else is marshalled and spliced in.

Situation Behaviour
Existing scalar, scalar value mutated in place; inline comment and quoting style survive
Existing collection, any value replaced wholesale; comments, anchors and block styles inside it are gone
Missing final segment appended as the last key of the deepest existing ancestor
Missing intermediate segments synthesised as nested mappings
Value under an &anchor replaced under the anchor, so *alias references keep resolving
Value that is an *alias refused, ErrUnsupported
Replacement that would orphan an alias refused, ErrUnsupported, naming the alias path
Path running through a non-mapping refused, ErrNotFound, naming the blocking prefix
Malformed path ErrInvalidPath

New keys are appended rather than inserted, which is what leaves the existing keys' comments and order untouched.

Newly synthesised nesting is indented two spaces per level regardless of the file's own indentation, because it comes from the marshaller rather than from the document. Existing indentation is never rewritten. A new key added to a four-space file lands at four spaces; a new sub-mapping under it lands at six.

Setting a string over an existing plain scalar has a sharp edge — see Assigning a string that needs quoting before you pass user input to Set.

Document.Remove

func (d *Document) Remove(path string) error

Deletes the entry at path and the comments that belong to it. Removing a key removes everything beneath it.

Situation Behaviour
Path resolves the entry is removed, nil
Path does not resolve ErrNotFound — removing something already absent is an error, not a no-op
Malformed path ErrInvalidPath
The subtree defines an anchor an alias still uses refused, ErrUnsupported, naming the alias that would dangle
The entry was the last in its mapping the mapping becomes {} — the parent is never removed for you

Which nearby comments go with the key is decided positionally; the rules are in Comment ownership, and their one failure case is in Limitations.

Node

A read-only view of one addressed entry. A value type — copying it is free and copies nothing but a pointer to the parsed entry.

Node.Path

func (n Node) Path() string

The dotted path the node was addressed by, exactly as it was passed to Get.

Node.Position

func (n Node) Position() Position

Where the node's key starts in the source. Line and Column are both 1-based. A key with no token yields the zero Position{0, 0}.

Positions describe the source that was parsed. After a Set that adds or replaces nodes, positions of untouched entries are still the original ones, and positions inside newly built nodes are synthetic.

Node.Comments

func (n Node) Comments() Comments

The comments attached to the node, with the leading # and surrounding whitespace stripped — the marker is syntax, not content.

Head is every comment line above the key, one entry per line. It does not apply the ownership rules Remove uses: a file header or a section heading separated by a blank line still appears in the Head of the key below it. If you need "the comment that belongs to this key", the blank-line rule is yours to apply.

Line is the comment on the key's own line, empty when there is none.

Node.String

func (n Node) String() string

The node's value as text. Quoting is syntax, so it is not part of the value: a scalar written "hello # world" reads back as hello # world, hash included, because inside quotes that hash is a literal character.

Value in the document String() returns
plain or "quoted" or 'single' the decoded scalar, unquoted, escapes resolved
A \| or > block the block's content, without the indicator or its indentation
An integer, float or boolean the token text as written
a: with nothing after it null
~ ~
A mapping or sequence the collection rendered as it appears in the source
*alias *alias — the reference, unresolved
A tagged value such as !!str 5 !!str 5 — the tag is included in the text

Node.IsCollection

func (n Node) IsCollection() bool

True when the value is a mapping or a sequence, in block or flow style. False for scalars, nulls and aliases — including an alias that resolves to a mapping, since the node itself is a reference.

Comments

type Comments struct {
    Head []string // comment block above the key, one entry per line
    Line string   // comment on the key's own line, empty when there is none
}

Position

type Position struct {
    Line   int // 1-based
    Column int // 1-based
}

Unsupported

type Unsupported struct {
    Reason Reason
    Line   int    // 1-based line where the construct starts
    Detail string
}

func (u Unsupported) String() string

String() formats as line 12: <reason> (<detail>), dropping the parenthesised detail when there is none. The two Reason values and what triggers them are in Unsupported constructs.

Errors

Five sentinels, all matched with errors.Is. Each has its own section in Errors, including what to do about it.

var (
    ErrNotFound    = errors.New("yamldoc: path not found")
    ErrUnsupported = errors.New("yamldoc: document contains an unsupported construct")
    ErrInvalidPath = errors.New("yamldoc: invalid path")
    ErrEmitInvalid = errors.New("yamldoc: refusing to emit invalid YAML")
    ErrInternal    = errors.New("yamldoc: internal invariant violated")
)