oneyaml

package module
v0.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 16, 2026 License: Apache-2.0, MIT Imports: 25 Imported by: 0

README

oneyaml-go

Pure-Go YAML 1.2 that edits a file and keeps its comments.

Go Reference CI Go version License

Spec-correct YAML 1.2 by default. Edits change only the value you set, in its own style; comments and formatting elsewhere are kept byte for byte. Nesting, alias expansion, number size and patch work are bounded by default.

Benchmark results will come in a later update, measured with the oneyaml-kit harness.

  • Spec-correct YAML 1.2 by default. Plain scalars follow the Core schema, so yes is a string and 0755 is the integer 755. YAML 1.1 forms come only from the compat packages. Results against the yaml-test-suite are under Correctness.
  • Minimal-diff edits. Document.Set rewrites one value in its own style and falls back to the nearest style that can hold the new value. Reading it back always gives exactly the value you set. See Editing.
  • Patches. RFC 6902 JSON Patch, with test comparing values by the YAML data model (so 1 and 1.0 differ), RFC 6901 pointers and RFC 7396 merge patch apply to a YAML file atomically and keep its layout. See Patches.
  • Bounded by default. Nesting depth, alias expansion, the digits of a number and the work of a patch are bounded unless you pass NoLimits(). Input size, documents per stream and scalar length are bounded only with Untrusted(); select it, and size it with Limits, for input you do not control. Your own hooks run outside these bounds, and the compat packages have no Untrusted() mode. A panic caused by input is treated as a security bug. No unsafe and no cgo. See Limits.
  • Compatibility adapters for yaml.v3 and sigs.k8s.io/yaml. One import change keeps your call sites; the known differences are listed. See Migrating from yaml.v3 and from sigs.k8s.io/yaml.
  • No dependencies outside the standard library.

Install

go get oneyaml.dev/go@latest

Quick start

Change one value in a file. The comment, the block scalar and every other byte stay as they were. (The examples import fmt, log and oneyaml "oneyaml.dev/go".)

src := []byte(`# deploy settings
name: api
replicas: 2   # raised by the release job
script: |
  make build
`)

doc, err := oneyaml.Parse(src)
if err != nil {
	log.Fatal(err)
}
if err := doc.Set("/replicas", 3); err != nil {
	log.Fatal(err)
}
if err := doc.Set("/script", "make build\nmake test\n"); err != nil {
	log.Fatal(err)
}
fmt.Print(string(doc.Bytes()))
// Output:
// # deploy settings
// name: api
// replicas: 3   # raised by the release job
// script: |
//   make build
//   make test

Reading into a struct works as it does in encoding/json:

src := []byte(`# deploy settings
name: api
replicas: 2
ports: [80, 443]
labels:
  tier: web
`)
var cfg struct {
	Name     string            `yaml:"name"`
	Replicas int               `yaml:"replicas"`
	Ports    []int             `yaml:"ports"`
	Labels   map[string]string `yaml:"labels"`
}
if err := oneyaml.Unmarshal(src, &cfg); err != nil {
	log.Fatal(err)
}
fmt.Println(cfg.Name, cfg.Replicas, cfg.Ports, cfg.Labels)
// Output: api 2 [80 443] map[tier:web]

The package documentation on pkg.go.dev has a runnable example for every feature.

When to use it, and when not to

Use it when you need to change a YAML file that people also edit by hand, apply a JSON Patch to one, read YAML 1.2 as the specification says, or read input you do not control with Untrusted().

Think twice when:

  • You need to edit comments. A Node reads comments, but there is no API to change them yet.
  • You need to stream events. The parser is not exposed; Parse and NewDecoder read a whole document.
  • You hold very large files in memory. A Document keeps the text and an index of it, which is an order of magnitude larger than the text for dense input. Set MaxInputBytes, or use Untrusted(), for input from outside.
  • Your data uses a collection as a mapping key and you read it into any. A Go map cannot hold it; a Document can.
  • Your program depends on go-yaml quirks. The compat packages reproduce documented behaviour, not bugs, and cover yaml.v3 and sigs.k8s.io/yaml only.

Comparison says where other libraries are the better choice.

Correctness

Conformance is measured with oneyaml-kit, the conformance kit shared by the oneyaml libraries, against the yaml-test-suite release data-2022-01-17. The rows below are generated. Conformance has the whole report, explains what each column means, including the strict column for invalid input, and lists every known deviation.

Method: kit 0.1.0 (corpus digest 3128b9207c00, kit revision 4777c7d2370d), every case including the slow ones, run 2026-10-16, library revision 813058186b9a, yaml-test-suite data-2022-01-17 (revision 6e6c296a) plus 574 kit cases. Generated by bin/kit run --slow --adapter oneyaml-go from the kit's result file oneyaml-go.json; do not edit.

Group Cases Loose Strict Strict, corrected Unsupported Strict only Emulated
yaml-test-suite · events · invalid input 94 94/94 0/94 0/94 0 94 0
yaml-test-suite · events · valid input 308 308/308 308/308 – 0 0 0
yaml-test-suite · values (in.json) · valid input 279 278/279 278/279 279/279 0 0 0
yaml-test-suite · events · all 402 402 402/402 308/402 308/402 0 94 0

The library is fuzzed with Go's native fuzzing, and its patch and compat packages are checked against their targets by differential tests.

Compatibility adapters for yaml.v3 and sigs.k8s.io/yaml

The compat packages are adapters with the same names and signatures as their targets, so changing the import path keeps your call sites; the package name stays yaml.

# gopkg.in/yaml.v3  ->  oneyaml.dev/go/compat/yamlv3
# sigs.k8s.io/yaml  ->  oneyaml.dev/go/compat/k8syaml

They read YAML 1.1 forms the way their targets do, but they are not identical. Known gaps and differences:

  • yaml.v3: a repeated key in a mapping that the target ignores (a struct with no such field) is not reported. Some comment layouts, such as comment lines inside a flow collection, are not placed or written back as yaml.v3 does.
  • Numbers: in compat/yamlv3, a plain 3.5 read into an integer field is a type error instead of being truncated. In compat/k8syaml, a float read into a string field keeps its full precision, where sigs.k8s.io/yaml rounds it to 32 bits.
  • Valid and invalid input: both read valid YAML that their targets refuse, compat/yamlv3 refuses invalid YAML that yaml.v3 accepts, and both return errors where their targets panic.
  • Limits: both always run with the default limits and have no Untrusted() mode.

Each package's documentation lists every difference (yamlv3, k8syaml), and the migration guides have a checklist. New code should use the oneyaml package.

Documentation

You want to Read
Look up a function or type pkg.go.dev
Change a value and keep the file's style Editing
Apply a JSON Patch or merge patch Patches
Read input you do not control Limits, Security
Know what is and is not conformant Conformance
Move from yaml.v3 Migrating from yaml.v3
Move from sigs.k8s.io/yaml Migrating from sigs.k8s.io/yaml
Compare with other Go libraries Comparison
See what changed Changelog
Know what is stable and which Go versions are supported Stability
Contribute Contributing

Stability

This is v0, the first series of releases. The API may still change before 1.0, but only in a minor release (0.1 to 0.2, say), never in a patch release. Every change that can break your code is listed in the changelog and marked Breaking, with what to write instead, so read it before you move to a new minor version.

The minimum Go version is 1.24 (go 1.24 in go.mod). CI tests Go 1.24, 1.25, 1.26 and 1.27. Raising the minimum takes a minor release and is announced in the changelog.

From 1.0, the module follows Go's rules for a stable module. Within a major version:

  • No change breaks code that uses the exported API of oneyaml.dev/go, compat/yamlv3 or compat/k8syaml. That covers the error types and the sentinel errors you match with errors.As and errors.Is; the text of error messages is written for people and may change in any release.
  • The bytes that Marshal writes with default options do not change, and neither do the bytes that an edit leaves untouched, except to fix output that is incorrect. Such a fix is listed in the changelog.

A change that would break these promises means a new major version with its own module path, oneyaml.dev/go/v2.

This is the only place this policy is stated; other pages link here.

License

Licensed under either of Apache License, Version 2.0 or MIT license, at your option. Unless you state otherwise, any contribution you submit for inclusion is dual licensed as above, without additional terms or conditions.

Documentation

Overview

Package oneyaml reads, writes and edits YAML. Spec-correct YAML 1.2 by default, in pure Go with no dependencies outside the standard library.

The package has two halves. The first is a typed load and dump in the style of encoding/json: Unmarshal, Marshal, NewDecoder and NewEncoder. The second is Document, which holds a whole YAML file and edits it in place. Edits change only the value you set, in its own style; comments and formatting elsewhere are kept byte for byte.

Nesting, alias expansion, number size and patch work are bounded by default; input from outside needs the Untrusted option as well. See "Limits" below.

Install

go get oneyaml.dev/go@latest

The import path is oneyaml.dev/go and the package name is oneyaml. This is a v0 release: the API may change before 1.0. The supported Go versions and what each release promises are in the Stability section of the README.

Load

Unmarshal reads the first document of its input into a Go value. Struct fields are matched through the yaml struct tag; with no yaml tag a json tag is used. A document read into any gives map[string]any, []any, string, bool, int, int64, uint64, math/big.Int and float64. Integers are never rounded. Plain scalars follow the YAML 1.2 Core schema: yes, no, on and off are strings, and 0755 is the integer 755. NewDecoder reads a stream of documents and returns io.EOF at the end.

A type can read its own YAML. The hooks are tried in this order: Unmarshaler, which receives a Node; UnmarshalYAML(func(any) error) error, the older form that yaml.v2, yaml.v3 and goccy/go-yaml accept; encoding.TextUnmarshaler, for a scalar; and encoding/json.Unmarshaler, which receives the value as JSON. So types written for encoding/json, such as Kubernetes API types with json tags and UnmarshalJSON methods, load as they are. An UnmarshalYAML method of any other form, such as yaml.v3's UnmarshalYAML(*yaml.Node) error, is reported as a *TypeError rather than skipped. A Node is not a decode target; read one with Document.Get.

Edit

Parse returns a Document that keeps every byte of its input. Document.Set assigns a value at an RFC 6901 pointer: it replaces an existing value, keeping its style when the style can hold the new value and moving to the nearest style that can when it cannot, and it adds a missing key at the end of its mapping in the style of its siblings. Reading the value back always gives exactly the value set. Document.Remove deletes an entry with the comments that belong to it. Document.Bytes and Document.WriteTo write the result. An edit that cannot be made, such as one that would write through an alias, returns an *EditError and leaves the document unchanged.

An edit makes every Node read before it stale (Node.Stale). A range over Node.Items or Node.Entries may edit the document, as a loop over a map may change the map: it finds its collection again and goes on with fresh nodes. Edits that add or remove entries may make such a loop skip or repeat some; for those, collect the places with Node.Pointer first and edit afterwards.

Document.Get and Document.Decode read values back through the same binder that Unmarshal uses, and Document.Root gives a read-only Node view with kinds, styles, tags, anchors, comments and positions.

Patch

Document.ApplyPatch applies an RFC 6902 JSON Patch whose test operation compares values by the YAML data model, so 1 and 1.0 differ (see the Document.ApplyPatch documentation). Document.Apply takes the same operations as Go values (PatchAdd, PatchReplace and the rest), and Document.ApplyMergePatch applies an RFC 7396 merge patch. A patch is atomic: if any operation fails, the document is unchanged. The edits it makes follow the same rules as Document.Set.

Marshal

Marshal and Encoder write Go values as block-style YAML. A string is written plain only when every common reader, under the Core schema and under YAML 1.1, reads it back as the same string, so "yes", "0755" and "1:30" come out quoted. Output is deterministic: map keys are sorted and struct fields keep their declaration order. A type can choose its own form by implementing Marshaler, encoding.TextMarshaler or encoding/json.Marshaler, tried in that order. A Node is written as the value it holds. The omitempty tag option leaves out zero values, empty collections and structs whose fields are all zero; omitzero leaves out only zero values. Indent sets the indentation.

Limits

Every call that reads input runs under limits on nesting depth, alias expansion, the digits of a number and the work of a patch; see DefaultLimits. They are on unless you pass NoLimits. Input size, scalar size and documents per stream are bounded only with Untrusted, which you should select, and size with Limits, for input from a source you do not control. Options apply in order: Limits changes only the fields it sets, on top of the options before it, and NoBound lifts one bound. So Untrusted(), Limits(LimitSet{MaxDepth: 50}) keeps every untrusted bound and tightens depth. A limit that is hit is a *LimitError. The Document and decoder paths follow aliases under the same budget. Code in your own hooks runs outside these limits, and the compat packages always use the defaults.

Errors

Errors are values to test with errors.As and errors.Is: *SyntaxError carries the line, column, offset and a stable category; *TypeError the path and position of a value that does not fit its target; *LimitError the limit and the option that raises it; and *EditError the operation, the pointer and a kind to match against ErrNotFound, ErrAliasWrite, ErrAmbiguousKey, ErrTestFailed, ErrAnchorInUse, ErrInvalidPatch, ErrInvalidPointer, ErrUndefinedAlias, ErrUnsupportedEdit, ErrStaleNode, ErrNoPointer and ErrNilDocument. Match errors by type and kind: the text of a message is written for people and may change in any release.

The library contains no unsafe code. A panic caused by input is treated as a security bug; report it as described in the security policy in the repository.

Options

One Option type serves every entry point, and an option that does not apply to a call is ignored: KnownFields makes an unknown struct key an error, MergeKeys reads the "<<" merge key, Indent sets the output indentation, and Limits, Untrusted and NoLimits set the limits, in the order given. Duplicate keys are always an error.

Compatibility

The compatibility adapters oneyaml.dev/go/compat/yamlv3 and oneyaml.dev/go/compat/k8syaml have the names and signatures of gopkg.in/yaml.v3 and sigs.k8s.io/yaml, so code that uses them keeps its call sites after one import change. They read YAML 1.1 forms the way their targets do, which this package never does, and each lists where it differs from its target.

More

The repository README has a quick start, and its docs directory has guides on editing, patches, limits, conformance, migration and a comparison with other Go YAML libraries.

Example (BlockLiteralKeepsStyle)

Case A: the new value ends in a line break, so the block literal keeps its style, its header and its place. Only the line of text changes.

package main

import (
	"fmt"
	"log"

	oneyaml "oneyaml.dev/go"
)

func main() {
	doc, err := oneyaml.Parse([]byte(`#hello
a: 10

#abc 123
b: |
  this is a test
c: 13
`))
	if err != nil {
		log.Fatal(err)
	}
	if err := doc.Set("/b", "this is a test2\n"); err != nil {
		log.Fatal(err)
	}
	fmt.Print(string(doc.Bytes()))
}
Output:
#hello
a: 10

#abc 123
b: |
  this is a test2
c: 13
Example (BlockLiteralStrip)

Case B: the new value has no final line break. Reading the document back must give exactly that value, so the header changes to |- and nothing else does.

package main

import (
	"fmt"
	"log"

	oneyaml "oneyaml.dev/go"
)

func main() {
	doc, err := oneyaml.Parse([]byte(`#hello
a: 10

#abc 123
b: |
  this is a test
c: 13
`))
	if err != nil {
		log.Fatal(err)
	}
	if err := doc.Set("/b", "this is a test2"); err != nil {
		log.Fatal(err)
	}
	fmt.Print(string(doc.Bytes()))

	var b string
	if err := doc.Get("/b", &b); err != nil {
		log.Fatal(err)
	}
	fmt.Printf("%q\n", b)
}
Output:
#hello
a: 10

#abc 123
b: |-
  this is a test2
c: 13
"this is a test2"
Example (FromYAMLv3Forms)

Three yaml.v3 habits that differ. 0755 is the decimal integer 755, a date is a string in a generic value, and the "<<" key is an ordinary key unless MergeKeys asks for the merge.

package main

import (
	"fmt"
	"log"

	oneyaml "oneyaml.dev/go"
)

func main() {
	src := []byte("mode: 0755\nwhen: 2001-02-03\nbase: &b {a: 1}\nuse:\n  <<: *b\n")
	var plain, merged map[string]any
	if err := oneyaml.Unmarshal(src, &plain); err != nil {
		log.Fatal(err)
	}
	if err := oneyaml.Unmarshal(src, &merged, oneyaml.MergeKeys()); err != nil {
		log.Fatal(err)
	}
	fmt.Printf("%v %q\n", plain["mode"], plain["when"])
	fmt.Println(plain["use"])
	fmt.Println(merged["use"])

	out, err := oneyaml.Marshal(struct{ Tags []string }{})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Print(string(out))
}
Output:
755 "2001-02-03"
map[<<:map[a:1]]
map[a:1]
tags: null
Example (FromYAMLv3Node)

The yaml.v3 way to change a value and keep the comments is to walk a yaml.Node tree. A Document does it with a pointer.

package main

import (
	"fmt"
	"log"

	oneyaml "oneyaml.dev/go"
)

func main() {
	src := "# service settings\nname: api # public name\nreplicas: 2\n"
	doc, err := oneyaml.Parse([]byte(src))
	if err != nil {
		log.Fatal(err)
	}
	if err := doc.Set("/replicas", 3); err != nil {
		log.Fatal(err)
	}
	fmt.Print(string(doc.Bytes()))
}
Output:
# service settings
name: api # public name
replicas: 3
Example (QuickStart)

The quick start: Set rewrites a value in its own style and leaves the comments and every other byte as they were.

package main

import (
	"fmt"
	"log"

	oneyaml "oneyaml.dev/go"
)

func main() {
	src := []byte(`# deploy settings
name: api
replicas: 2   # raised by the release job
script: |
  make build
`)

	doc, err := oneyaml.Parse(src)
	if err != nil {
		log.Fatal(err)
	}
	if err := doc.Set("/replicas", 3); err != nil {
		log.Fatal(err)
	}
	if err := doc.Set("/script", "make build\nmake test\n"); err != nil {
		log.Fatal(err)
	}
	fmt.Print(string(doc.Bytes()))
}
Output:
# deploy settings
name: api
replicas: 3   # raised by the release job
script: |
  make build
  make test

Index

Examples

Constants

View Source
const NoBound = -1

NoBound, used as the value of a LimitSet field, removes the bound for that field alone. Any negative value has the same effect; NoBound gives it a name.

Variables

View Source
var (
	// ErrNotFound means a pointer or path addressed nothing.
	ErrNotFound = errors.New("oneyaml: path not found")
	// ErrNilDocument means a method that changes or decodes a document was called on
	// a nil *Document, which is what a failed Parse returns.
	ErrNilDocument = errors.New("oneyaml: nil Document")
	// ErrAliasWrite means an edit would write through an alias.
	ErrAliasWrite = errors.New("oneyaml: write through an alias")
	// ErrAmbiguousKey means a key occurs more than once, so an edit cannot choose.
	ErrAmbiguousKey = errors.New("oneyaml: ambiguous key")
	// ErrTestFailed means a patch "test" operation did not match.
	ErrTestFailed = errors.New("oneyaml: patch test failed")
	// ErrAnchorInUse means an anchor cannot be removed while an alias uses it.
	ErrAnchorInUse = errors.New("oneyaml: anchor in use")
)

Sentinel errors for the kinds of failure a document operation can report. Match them with errors.Is.

View Source
var ErrInvalidPatch = errors.New("oneyaml: invalid patch")

ErrInvalidPatch means a patch is not well formed: it is not valid JSON, it is not an array of operations, an operation is unknown or lacks a member it needs, or the operation cannot be done at all (moving a value into itself).

View Source
var ErrInvalidPointer = errors.New("oneyaml: invalid JSON pointer")

ErrInvalidPointer means a string is not an RFC 6901 JSON Pointer.

View Source
var ErrNoPointer = errors.New("oneyaml: node has no pointer")

ErrNoPointer means a Node has no RFC 6901 pointer: it is a mapping key, lies inside a complex key, or its pointer would not lead back to it.

View Source
var ErrStaleNode = errors.New("oneyaml: node is stale: the document was edited after it was read")

ErrStaleNode means a Node was used after the Document it came from was edited. Fetch the node again after the edit.

View Source
var ErrUndefinedAlias = errors.New("oneyaml: alias has no anchor in its document")

ErrUndefinedAlias means a value to be written holds an alias whose anchor is not in the document it was read from, so there is nothing to write in its place.

View Source
var ErrUnsupportedEdit = errors.New("oneyaml: unsupported edit")

ErrUnsupportedEdit means the edit is valid but the document layout is one the edit engine does not rewrite. The document is left unchanged.

Functions

func Marshal

func Marshal(v any, opts ...Option) ([]byte, error)

Marshal returns v as a YAML document.

Output is block style with a two-space indent, ends with a newline and has no document marker. Strings are written plain only when every common reader would read them back as the same string; "yes", "0755" and "1:30" are quoted. Map keys are sorted and struct fields follow declaration order, so the same value always gives the same bytes.

Struct fields are named by the yaml tag, or by the json tag when there is no yaml tag, or by the lower-cased field name. The tag options are omitempty, omitzero, inline and flow. A nil map, slice, pointer or interface is written as null.

Types may implement Marshaler, encoding.TextMarshaler or json.Marshaler, tried in that order. The JSON a json.Marshaler returns is written as the equivalent YAML value (a JSON string as a plain string where it is safe, an object as a mapping with sorted keys). A type with a MarshalYAML method that is not MarshalYAML() (any, error) is a *TypeError. A Node is written as its value. A struct field tagged omitempty is left out when it is a struct whose fields are all zero, or whose IsZero method returns true.

time.Time is written as an RFC 3339 timestamp, time.Duration as its String form, *big.Int as an integer, and []byte as a !!binary scalar. A string that is not valid UTF-8 is also written as !!binary, because no other form keeps its bytes; some readers (goccy) return such a scalar as []byte rather than a string.

Marshal reports a *TypeError for a value with no YAML form (channels, functions, complex numbers) and a *LimitError when nesting or a cycle exceeds the depth limit.

Example

Strings that another reader could take for a number, boolean or null are quoted, so the text reads back as the same string everywhere.

package main

import (
	"fmt"

	oneyaml "oneyaml.dev/go"
)

func main() {
	type Server struct {
		Name    string   `yaml:"name"`
		Port    int      `yaml:"port"`
		Enabled bool     `yaml:"enabled"`
		Tags    []string `yaml:"tags,omitempty"`
		Version string   `yaml:"version"`
		Notes   string   `yaml:"notes"`
	}
	out, err := oneyaml.Marshal(Server{
		Name:    "api",
		Port:    8080,
		Enabled: true,
		Tags:    []string{"web", "yes"},
		Version: "1.10",
		Notes:   "first line\nsecond line\n",
	})
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Print(string(out))
}
Output:
name: api
port: 8080
enabled: true
tags:
  - web
  - 'yes'
version: '1.10'
notes: |
  first line
  second line
Example (JsonTags)

Marshal honours json tags when a field has no yaml tag, and writes struct fields in declaration order, where a round trip through JSON sorts them.

package main

import (
	"fmt"
	"log"

	oneyaml "oneyaml.dev/go"
)

func main() {
	type Container struct {
		Name  string   `json:"name"`
		Image string   `json:"image"`
		Args  []string `json:"args,omitempty"`
	}
	out, err := oneyaml.Marshal(Container{Name: "web", Image: "nginx:1.25", Args: []string{"--port", "8080"}})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Print(string(out))
}
Output:
name: web
image: nginx:1.25
args:
  - --port
  - '8080'
Example (Map)

Map keys are written in sorted order, so output is the same on every run.

package main

import (
	"fmt"
	"log"

	oneyaml "oneyaml.dev/go"
)

func main() {
	out, err := oneyaml.Marshal(map[string]any{
		"zebra": 1,
		"apple": []any{1.5, nil, "x: y"},
		"empty": map[string]any{},
	})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Print(string(out))
}
Output:
apple:
  - 1.5
  - null
  - 'x: y'
empty: {}
zebra: 1
Example (Omitempty)

omitempty leaves out zero values, empty slices, maps and strings, and a struct whose fields are all zero. omitzero leaves out only zero values, so an empty but non-nil slice is kept.

package main

import (
	"fmt"
	"log"

	oneyaml "oneyaml.dev/go"
)

func main() {
	type Resources struct {
		CPU    string `yaml:"cpu"`
		Memory string `yaml:"memory"`
	}
	out, err := oneyaml.Marshal(struct {
		Name      string    `yaml:"name"`
		Resources Resources `yaml:"resources,omitempty"`
		Args      []string  `yaml:"args,omitempty"`
		Env       []string  `yaml:"env,omitzero"`
	}{Name: "api", Args: []string{}, Env: []string{}})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Print(string(out))
}
Output:
name: api
env: []

func Unmarshal

func Unmarshal(data []byte, v any, opts ...Option) error

Unmarshal reads the first YAML document in data into the value v points to.

v must be a non-nil pointer. Structs are filled by key (see the tag rules on Marshal); maps, slices, arrays, pointers, interfaces and the scalar kinds are filled by shape. A plain scalar is read with the YAML 1.2 Core schema, so "yes" and "0755" are strings. A quoted scalar is always a string, and an explicit tag such as !!int or !!str overrides the resolution. A null value leaves a struct field, number or string as it was and clears a pointer, map, slice or interface.

Into an interface{} a mapping becomes map[string]any, or map[any]any when a key is not a string; a sequence becomes []any; an integer becomes int, or uint64 or *big.Int when it does not fit; a float becomes float64; null becomes nil; and everything else is a string, timestamps and !!binary text included. A tag the Core schema does not resolve is ignored.

Types may read themselves. The hooks are tried in this order: Unmarshaler (UnmarshalYAML with a Node), the older form UnmarshalYAML(func(any) error) error that yaml.v2, yaml.v3 and goccy use, encoding.TextUnmarshaler for a scalar, then json.Unmarshaler. A json.Unmarshaler is given the value as JSON text, so types written for encoding/json (Kubernetes quantities, say) need no YAML code; a value JSON cannot hold, such as a mapping with a key that is not a string or .nan, is a *TypeError. A type with an UnmarshalYAML method of any other form, yaml.v3's UnmarshalYAML(*yaml.Node) error for one, is a *TypeError and is not filled by its fields. A null value clears a pointer target without calling any hook; of the other targets, only an Unmarshaler is called for a null value. time.Time reads a timestamp, time.Duration its String form, *big.Int any integer, and []byte a !!binary scalar (or the text of any other scalar).

A Node is a view into a Document and is not a target: decoding into one is a *TypeError. Read it with Document.Get(pointer, &node).

A mapping key that occurs twice is an error, judged after resolution, so 1 and 0x1 clash. Merge keys are ordinary keys unless the MergeKeys option is given. Unknown struct keys are skipped unless KnownFields is given.

Aliases are decoded by replaying the anchored node; the alias limits bound how much that may produce. If data holds more than one document only the first is read; if it holds none, v is left unchanged and the error is nil.

The error is a *SyntaxError for malformed input, a *TypeError for a value that does not fit its target (the first one found) and a *LimitError when a limit is exceeded. UTF-16 and UTF-32 input, with or without a byte order mark, is converted to UTF-8 first; the offsets of errors in such input index the UTF-8 form.

Example
src := []byte(`# deploy settings
name: api
replicas: 2
ports: [80, 443]
labels:
  tier: web
`)
var cfg struct {
	Name     string            `yaml:"name"`
	Replicas int               `yaml:"replicas"`
	Ports    []int             `yaml:"ports"`
	Labels   map[string]string `yaml:"labels"`
}
if err := oneyaml.Unmarshal(src, &cfg); err != nil {
	log.Fatal(err)
}
fmt.Println(cfg.Name, cfg.Replicas, cfg.Ports, cfg.Labels)
Output:
api 2 [80 443] map[tier:web]
Example (CollectionKey)

Generic values hold what Go maps can hold. A collection used as a mapping key cannot be a Go map key, so Unmarshal into any refuses it; a Document reads it.

package main

import (
	"fmt"
	"log"

	oneyaml "oneyaml.dev/go"
)

func main() {
	src := []byte("[a, b]: c\n")
	var v any
	fmt.Println(oneyaml.Unmarshal(src, &v) != nil)

	doc, err := oneyaml.Parse(src)
	if err != nil {
		log.Fatal(err)
	}
	key, value := doc.Root().Entry(0)
	fmt.Println(key.Kind(), key.Len(), value.Value())
}
Output:
true
sequence 2 c
Example (FuncHook)
package main

import (
	"fmt"
	"log"
	"strings"

	oneyaml "oneyaml.dev/go"
)

// Tags reads either a sequence or a comma-separated string. It uses the
// UnmarshalYAML form of yaml.v2, yaml.v3 and goccy, so code written for them
// keeps working.
type Tags []string

func (t *Tags) UnmarshalYAML(unmarshal func(any) error) error {
	var list []string
	if err := unmarshal(&list); err == nil {
		*t = list
		return nil
	}
	var one string
	if err := unmarshal(&one); err != nil {
		return err
	}
	*t = strings.Split(one, ",")
	return nil
}

func main() {
	var doc struct {
		A Tags `yaml:"a"`
		B Tags `yaml:"b"`
	}
	if err := oneyaml.Unmarshal([]byte("a: [x, y]\nb: \"p,q,r\"\n"), &doc); err != nil {
		log.Fatal(err)
	}
	fmt.Println(doc.A, doc.B)
}
Output:
[x y] [p q r]
Example (Generic)
var v any
err := oneyaml.Unmarshal([]byte("a: 1\nb: [x, 2.5, yes, null]\n"), &v)
fmt.Println(v, err)
Output:
map[a:1 b:[x 2.5 yes <nil>]] <nil>
Example (Json)

JSON is YAML, so a JSON document reads with Unmarshal as it is, and integers above 2^53 stay exact.

package main

import (
	"fmt"
	"log"

	oneyaml "oneyaml.dev/go"
)

func main() {
	var v map[string]any
	if err := oneyaml.Unmarshal([]byte(`{"id": 12345678901234567890, "ok": true}`), &v); err != nil {
		log.Fatal(err)
	}
	fmt.Println(v["id"], v["ok"])
}
Output:
12345678901234567890 true
Example (JsonHook)

A type that implements json.Unmarshaler or json.Marshaler and no YAML hook is read and written through that hook: the YAML value becomes JSON text first.

package main

import (
	"encoding/json"
	"fmt"
	"log"
	"strings"

	oneyaml "oneyaml.dev/go"
)

// Level is written for encoding/json: it reads and writes the words "low" and
// "high" and nothing else.
type Level int

func (l *Level) UnmarshalJSON(b []byte) error {
	var s string
	if err := json.Unmarshal(b, &s); err != nil {
		return err
	}
	switch strings.ToLower(s) {
	case "low":
		*l = 1
	case "high":
		*l = 2
	default:
		return fmt.Errorf("unknown level %q", s)
	}
	return nil
}

func (l Level) MarshalJSON() ([]byte, error) {
	return json.Marshal([]string{"none", "low", "high"}[l])
}

// A type that implements json.Unmarshaler or json.Marshaler and no YAML hook is
// read and written through that hook: the YAML value becomes JSON text first.
func main() {
	var cfg struct {
		Level Level `yaml:"level"`
	}
	if err := oneyaml.Unmarshal([]byte("level: high\n"), &cfg); err != nil {
		log.Fatal(err)
	}
	fmt.Println(cfg.Level)

	out, err := oneyaml.Marshal(cfg)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Print(string(out))

	// The error from UnmarshalJSON comes back inside a *TypeError.
	err = oneyaml.Unmarshal([]byte("level: loud\n"), &cfg)
	fmt.Println(err)
}
Output:
2
level: high
oneyaml: line 1, column 8: /level: cannot unmarshal "loud" into oneyaml_test.Level: unknown level "loud"
Example (JsonTags)

Types that carry only json tags, as Kubernetes types do, read without a yaml tag; any yaml tag on a field takes the place of its json tag.

package main

import (
	"fmt"
	"log"

	oneyaml "oneyaml.dev/go"
)

func main() {
	var c struct {
		Name  string `json:"name"`
		Image string `json:"image,omitempty"`
	}
	if err := oneyaml.Unmarshal([]byte("name: web\nimage: nginx:1.25\n"), &c); err != nil {
		log.Fatal(err)
	}
	fmt.Println(c.Name, c.Image)
}
Output:
web nginx:1.25
Example (TypeError)
var cfg struct {
	Port int `yaml:"port"`
}
err := oneyaml.Unmarshal([]byte("name: api\nport: eighty\n"), &cfg)
var te *oneyaml.TypeError
if errors.As(err, &te) {
	fmt.Printf("%s at line %d, column %d: %s\n", te.Path, te.Line, te.Column, te.Msg)
}
Output:
/port at line 2, column 7: cannot unmarshal !!str `eighty` into int
Example (YesIsAString)

The primary API reads YAML 1.2: yes is a string, not a boolean, so a bool field refuses it. The compat packages read the way their targets do.

package main

import (
	"errors"
	"fmt"

	oneyaml "oneyaml.dev/go"
)

func main() {
	var cfg struct {
		Debug bool `yaml:"debug"`
	}
	err := oneyaml.Unmarshal([]byte("debug: yes\n"), &cfg)
	var te *oneyaml.TypeError
	if errors.As(err, &te) {
		fmt.Println(te.Path, te.Line, te.Column)
	}
}
Output:
/debug 1 8

Types

type Decoder

type Decoder struct {
	// contains filtered or unexported fields
}

A Decoder reads YAML documents from an input stream, one per call to Decode.

It reads ahead only as far as the end of the current document: a line that starts with "---" or "..." at column 0 ends a document wherever it occurs, since YAML forbids those markers inside any node. The document is then parsed from the buffer, so a Decoder never holds more than one document and the bytes after it.

Example
stream := "name: first\n---\nname: second\n---\nname: third\n"
dec := oneyaml.NewDecoder(strings.NewReader(stream))
for {
	var doc struct {
		Name string `yaml:"name"`
	}
	err := dec.Decode(&doc)
	if err == io.EOF {
		break
	}
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(doc.Name)
}
Output:
first
second
third

func NewDecoder

func NewDecoder(r io.Reader, opts ...Option) *Decoder

NewDecoder returns a Decoder that reads from r. The options are those of Unmarshal.

func (*Decoder) Decode

func (d *Decoder) Decode(v any) error

Decode reads the next document into the value v points to, as Unmarshal does. It returns io.EOF when there are no more documents. After a syntax error or a read error every later call returns the same error; after a *TypeError the next call reads the next document. An empty document decodes as null.

type Document

type Document struct {
	// contains filtered or unexported fields
}

A Document is a YAML stream held losslessly: every byte of the input, including comments, blank lines, indentation and line breaks, comes back from Bytes unless an edit changed it.

The methods address the first document of the stream; Len and Doc reach the others. A Document returned by Doc is a view that shares the stream: an edit through it changes the stream, and every view sees the change.

Edits are exact and minimal. Set writes the new value in the style of the value it replaces when that style can hold it, and falls back to the nearest style that can otherwise; text outside the edited value is not touched. An edit that fails leaves the document as it was. An edit makes every Node read before it stale; Node.Stale reports it. An edit, patch or merge patch that fails is rolled back completely and leaves the Nodes read before it valid. Read nodes again after editing, or collect their pointers first (Node.Pointer). A Document is not safe for concurrent use.

A nil *Document, which is what a failed Parse returns, is safe to call: getters return zero values (Len is 0, Bytes is nil, Root is the zero Node) and methods that decode, edit or patch return an error matching ErrNilDocument.

func Parse

func Parse(data []byte, opts ...Option) (*Document, error)

Parse reads data as a YAML stream and returns it as a Document. The data is copied; later changes to the slice do not affect the Document.

A stream with no documents (empty input) yields a Document of length 0. Syntax errors are reported as *SyntaxError and exceeded limits as *LimitError.

Example

Parse keeps the whole file. Set rewrites one value in its own style and leaves the comments, the blank lines and every other byte alone.

package main

import (
	"fmt"
	"log"

	oneyaml "oneyaml.dev/go"
)

func main() {
	src := []byte(`# deploy settings
name: api
replicas: 2   # raised by the release job
script: |
  make build
`)

	doc, err := oneyaml.Parse(src)
	if err != nil {
		log.Fatal(err)
	}
	if err := doc.Set("/replicas", 3); err != nil {
		log.Fatal(err)
	}
	if err := doc.Set("/script", "make build\nmake test\n"); err != nil {
		log.Fatal(err)
	}
	fmt.Print(string(doc.Bytes()))

	var replicas int
	if err := doc.Get("/replicas", &replicas); err != nil {
		log.Fatal(err)
	}
	fmt.Println(replicas)
}
Output:
# deploy settings
name: api
replicas: 3   # raised by the release job
script: |
  make build
  make test
3

func (*Document) Apply

func (d *Document) Apply(ops ...PatchOp) error

Apply applies RFC 6902 operations to the document in order, all or nothing, as ApplyPatch does.

Example

Apply takes the same operations as Go values.

package main

import (
	"fmt"
	"log"

	oneyaml "oneyaml.dev/go"
)

func main() {
	doc, err := oneyaml.Parse([]byte("a: 1\nb: 2\n"))
	if err != nil {
		log.Fatal(err)
	}
	err = doc.Apply(
		oneyaml.PatchTest("/a", 1),
		oneyaml.PatchMove("/b", "/c"),
		oneyaml.PatchAdd("/d", []string{"x", "y"}),
	)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Print(string(doc.Bytes()))
}
Output:
a: 1
c: 2
d:
  - x
  - y
Example (TestByValue)

A "test" operation compares by the YAML data model: 1 is an integer and 1.0 is a float, so they are different.

package main

import (
	"errors"
	"fmt"
	"log"

	oneyaml "oneyaml.dev/go"
)

func main() {
	doc, err := oneyaml.Parse([]byte("n: 1\n"))
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(doc.Apply(oneyaml.PatchTest("/n", 1)))
	err = doc.Apply(oneyaml.PatchTest("/n", 1.0))
	fmt.Println(errors.Is(err, oneyaml.ErrTestFailed))
}
Output:
<nil>
true

func (*Document) ApplyMergePatch

func (d *Document) ApplyMergePatch(rfc7396 []byte) error

ApplyMergePatch applies an RFC 7396 JSON Merge Patch, given as JSON text, to the document. A member of the patch that is null deletes the member of the same name; an object member is merged into the mapping it meets; any other value replaces what is there, and a sequence replaces the whole sequence. A patch that is not an object replaces the whole document.

The patch is atomic: on any error the document is left as it was, byte for byte. Members that the patch adds go at the end of their mapping in the style of their siblings, and comments and layout outside the changed values stay. A null in the target is kept unless the patch names it. Merging an object into a mapping that is an alias is refused (ErrAliasWrite) because it would change every other use of the anchor; an alias to something else is replaced.

The result may not be longer than a MaxInputBytes limit, the patch text may not either, and the text its edits have to parse again is bounded (MaxPatchWorkRatio); past a bound the call fails with a *LimitError and changes nothing.

The patch applies to the first document, or to the document a view from Doc selects.

Example

ApplyMergePatch applies an RFC 7396 merge patch: null deletes a member, an object merges into the mapping it meets, and anything else replaces.

package main

import (
	"fmt"
	"log"

	oneyaml "oneyaml.dev/go"
)

func main() {
	doc, err := oneyaml.Parse([]byte(`# settings
log:
  level: info   # default
  file: app.log
features: [a, b]
`))
	if err != nil {
		log.Fatal(err)
	}
	err = doc.ApplyMergePatch([]byte(`{"log": {"level": "debug", "file": null}, "features": ["c"], "debug": true}`))
	if err != nil {
		log.Fatal(err)
	}
	fmt.Print(string(doc.Bytes()))
}
Output:
# settings
log:
  level: debug   # default
features: [c]
debug: true
Example (Alias)

Merge patches replace arrays whole and refuse to merge into an alias.

package main

import (
	"errors"
	"fmt"
	"log"

	oneyaml "oneyaml.dev/go"
)

func main() {
	doc, err := oneyaml.Parse([]byte("base: &b {x: 1}\ncopy: *b\n"))
	if err != nil {
		log.Fatal(err)
	}
	err = doc.ApplyMergePatch([]byte(`{"copy": {"x": 2}}`))
	fmt.Println(errors.Is(err, oneyaml.ErrAliasWrite))
}
Output:
true

func (*Document) ApplyPatch

func (d *Document) ApplyPatch(rfc6902 []byte) error

ApplyPatch applies an RFC 6902 JSON Patch, given as JSON text, to the document. The patch is atomic: if any operation fails, including a "test" that does not match, the document is left exactly as it was, byte for byte, and the error is an *EditError that names the operation.

The operations follow the RFC, with these points where YAML needs a decision:

  • Pointers address mapping keys by their scalar text, and a key that occurs twice is ambiguous (ErrAmbiguousKey).
  • Reads follow aliases, including "from" of copy and the paths of "test". Writing below an alias is refused (ErrAliasWrite); replacing the alias node itself is allowed.
  • "test" compares by the YAML data model under the Core schema: 1 differs from 1.0, 0x1F equals 31, a quoted "1" is a string, an explicit core tag such as !!float decides the type of its scalar, and mappings are equal whatever the order of their keys.
  • "copy" writes the value without its anchor, so no anchor is defined twice. An alias inside the copied value stays an alias while its anchor is still defined once, before the destination; otherwise it is written out.
  • "move" refuses an anchored value that aliases still use (ErrAnchorInUse), and a destination inside the value being moved (ErrInvalidPatch).
  • What a patch may write is bounded. The text copies add up to at most the alias bytes limit (ten times the document, never less than 64 MiB by default), and with a MaxInputBytes limit no step may leave a document larger than it; a patch past either bound fails with a *LimitError and changes nothing. The text its edits have to parse again is bounded too (MaxPatchWorkRatio), and with a MaxInputBytes limit the patch text itself may not be longer than it. NoLimits removes the bounds.
  • New members go at the end of their mapping, in the style of their siblings; comments outside the edited values stay.

A patch applies to the first document, or to the document a view from Doc selects.

Example

ApplyPatch applies an RFC 6902 patch in one step. Comments and layout outside the changed values stay, and if any operation fails nothing changes.

package main

import (
	"errors"
	"fmt"
	"log"

	oneyaml "oneyaml.dev/go"
)

func main() {
	doc, err := oneyaml.Parse([]byte(`# service
name: api
replicas: 2   # raised by the release job
tags: [web]
`))
	if err != nil {
		log.Fatal(err)
	}
	err = doc.ApplyPatch([]byte(`[
		{"op": "test", "path": "/name", "value": "api"},
		{"op": "replace", "path": "/replicas", "value": 3},
		{"op": "add", "path": "/tags/-", "value": "edge"},
		{"op": "copy", "from": "/name", "path": "/title"}
	]`))
	if err != nil {
		log.Fatal(err)
	}
	fmt.Print(string(doc.Bytes()))

	// A failing test rejects the whole patch.
	err = doc.ApplyPatch([]byte(`[
		{"op": "replace", "path": "/replicas", "value": 9},
		{"op": "test", "path": "/name", "value": "web"}
	]`))
	var ee *oneyaml.EditError
	fmt.Println(errors.Is(err, oneyaml.ErrTestFailed), errors.As(err, &ee) && ee.Index == 1)
	fmt.Print(string(doc.Bytes()))
}
Output:
# service
name: api
replicas: 3   # raised by the release job
tags: [web, edge]
title: api
true true
# service
name: api
replicas: 3   # raised by the release job
tags: [web, edge]
title: api
Example (AliasRefused)

A patch never writes through an alias, and a refused patch changes nothing, not even the operations before the refused one.

package main

import (
	"errors"
	"fmt"
	"log"

	oneyaml "oneyaml.dev/go"
)

func main() {
	doc, err := oneyaml.Parse([]byte("base: &b\n  x: 1\ncopy: *b\n"))
	if err != nil {
		log.Fatal(err)
	}
	err = doc.ApplyPatch([]byte(`[
		{"op": "replace", "path": "/base/x", "value": 2},
		{"op": "replace", "path": "/copy/x", "value": 3}
	]`))
	fmt.Println(errors.Is(err, oneyaml.ErrAliasWrite))
	fmt.Print(string(doc.Bytes()))
}
Output:
true
base: &b
  x: 1
copy: *b
Example (PointerText)

A pointer token matches a key by its text, so /1 addresses the integer key 1. Complex keys cannot be addressed.

package main

import (
	"fmt"
	"log"

	oneyaml "oneyaml.dev/go"
)

func main() {
	doc, err := oneyaml.Parse([]byte("1: one\n\"2\": two\n"))
	if err != nil {
		log.Fatal(err)
	}
	err = doc.ApplyPatch([]byte(`[
		{"op": "replace", "path": "/1", "value": "uno"},
		{"op": "replace", "path": "/2", "value": "dos"}
	]`))
	if err != nil {
		log.Fatal(err)
	}
	fmt.Print(string(doc.Bytes()))
}
Output:
1: uno
"2": dos
Example (Stream)

A patch applies to one document of a stream: the one the Document addresses.

package main

import (
	"fmt"
	"log"

	oneyaml "oneyaml.dev/go"
)

func main() {
	doc, err := oneyaml.Parse([]byte("a: 1\n---\na: 1\n"))
	if err != nil {
		log.Fatal(err)
	}
	if err := doc.Doc(1).ApplyPatch([]byte(`[{"op": "replace", "path": "/a", "value": 2}]`)); err != nil {
		log.Fatal(err)
	}
	fmt.Print(string(doc.Bytes()))
}
Output:
a: 1
---
a: 2

func (*Document) Bytes

func (d *Document) Bytes() []byte

Bytes returns the stream as text. The slice must not be modified. It stays valid after later edits, which build new text instead of changing it.

func (*Document) Decode

func (d *Document) Decode(v any) error

Decode reads the document into the value v points to, with the rules and errors of Unmarshal: the same binder reads the stored tree that Unmarshal reads from the parser, so the two always agree. Aliases are expanded within the alias and depth limits the Document was parsed with. A Document with no documents leaves v unchanged. If the view addresses a document that does not exist, the error is an *EditError matching ErrNotFound.

Example
package main

import (
	"fmt"
	"log"

	oneyaml "oneyaml.dev/go"
)

func main() {
	doc, err := oneyaml.Parse([]byte("server:\n  host: example.org\n  ports: [80, 443]\n"))
	if err != nil {
		log.Fatal(err)
	}
	var cfg struct {
		Server struct {
			Host  string `yaml:"host"`
			Ports []int  `yaml:"ports"`
		} `yaml:"server"`
	}
	if err := doc.Decode(&cfg); err != nil {
		log.Fatal(err)
	}
	fmt.Println(cfg.Server.Host, cfg.Server.Ports)
}
Output:
example.org [80 443]

func (*Document) Doc

func (d *Document) Doc(i int) *Document

Doc returns a view of document i of the stream. The view shares the stream with d. If i is out of range every operation on the view reports ErrNotFound.

Example

A stream of several documents is one Document; Doc addresses one of them.

package main

import (
	"fmt"
	"log"

	oneyaml "oneyaml.dev/go"
)

func main() {
	doc, err := oneyaml.Parse([]byte("a: 1\n---\nb: 2\n"))
	if err != nil {
		log.Fatal(err)
	}
	if err := doc.Doc(1).Set("/c", 3); err != nil {
		log.Fatal(err)
	}
	fmt.Println(doc.Len())
	fmt.Print(string(doc.Bytes()))
}
Output:
2
a: 1
---
b: 2
c: 3

func (*Document) ExplicitEnd

func (d *Document) ExplicitEnd() bool

ExplicitEnd reports whether the document ends with a "..." marker. It is false for a view of a document that does not exist.

func (*Document) ExplicitStart

func (d *Document) ExplicitStart() bool

ExplicitStart reports whether the document begins with a "---" marker. A document without the marker is the first of its stream, or follows one that ended with "...". It is false for a view of a document that does not exist.

func (*Document) Get

func (d *Document) Get(ptr string, v any) error

Get reads the value at the RFC 6901 pointer ptr into v, as Decode does for the whole document: v is any target Unmarshal accepts. Aliases are followed, and expanding them is charged to the alias limits. A *TypeError's Path is the pointer ptr followed by the path inside the value.

If v is a *Node it receives the node itself, and the alias at the end of the pointer, if there is one, is not followed.

Example

Get reads a value by pointer; aliases are followed.

package main

import (
	"fmt"
	"log"

	oneyaml "oneyaml.dev/go"
)

func main() {
	doc, err := oneyaml.Parse([]byte("base: &b\n  port: 8080\nreplica: *b\n"))
	if err != nil {
		log.Fatal(err)
	}
	var port int
	if err := doc.Get("/replica/port", &port); err != nil {
		log.Fatal(err)
	}
	fmt.Println(port)
}
Output:
8080
Example (Node)

A Node is a view into a Document, so it is read with Document.Get, never decoded into. Marshal writes a Node as the value it holds.

package main

import (
	"errors"
	"fmt"
	"log"

	oneyaml "oneyaml.dev/go"
)

func main() {
	src := []byte("name: api\nspec: {replicas: 2, tier: web}\n")

	var n oneyaml.Node
	err := oneyaml.Unmarshal(src, &n)
	var te *oneyaml.TypeError
	fmt.Println(errors.As(err, &te))

	doc, err := oneyaml.Parse(src)
	if err != nil {
		log.Fatal(err)
	}
	if err := doc.Get("/spec", &n); err != nil {
		log.Fatal(err)
	}
	fmt.Println(n.Kind(), n.Len())

	out, err := oneyaml.Marshal(struct {
		Spec oneyaml.Node `yaml:"spec"`
	}{n})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Print(string(out))
}
Output:
true
mapping 2
spec:
  replicas: 2
  tier: web

func (*Document) Len

func (d *Document) Len() int

Len returns the number of documents in the stream.

func (*Document) Remove

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

Remove deletes the value at ptr, with its key or its place in the sequence. A comment on the line of the entry, a comment directly above it and a comment inside its value go with it. Removing the last entry of a nested collection leaves {} or []. Removing an anchored node that an alias still uses is refused (ErrAnchorInUse).

Example

Remove deletes an entry together with the comment on its line, the comment directly above it and any comment inside its value. A comment above the next entry stays.

package main

import (
	"fmt"
	"log"

	oneyaml "oneyaml.dev/go"
)

func main() {
	doc, err := oneyaml.Parse([]byte("a: 1\n# about b\nb: 2 # two\n# about c\nc: 3\n"))
	if err != nil {
		log.Fatal(err)
	}
	if err := doc.Remove("/b"); err != nil {
		log.Fatal(err)
	}
	fmt.Print(string(doc.Bytes()))
}
Output:
a: 1
# about c
c: 3
Example (AnchorInUse)

An edit that would leave an alias without its anchor is refused, and the document is unchanged.

package main

import (
	"errors"
	"fmt"
	"log"

	oneyaml "oneyaml.dev/go"
)

func main() {
	doc, err := oneyaml.Parse([]byte("base: &b\n  x: 1\ncopy: *b\n"))
	if err != nil {
		log.Fatal(err)
	}
	err = doc.Remove("/base")
	fmt.Println(errors.Is(err, oneyaml.ErrAnchorInUse))
	fmt.Print(string(doc.Bytes()))
}
Output:
true
base: &b
  x: 1
copy: *b

func (*Document) Root

func (d *Document) Root() Node

Root returns the root node of the document. The zero Node is returned if the document does not exist.

func (*Document) Set

func (d *Document) Set(ptr string, v any) error

Set replaces the value at the RFC 6901 pointer ptr with v, or adds it when the pointer names a mapping key that does not exist or the end of a sequence ("-" or the length).

v is anything Marshal accepts. The value that is replaced keeps its style (plain, quoted, literal or folded block, flow or block) when that style can hold the new value; otherwise only that value falls back to the nearest style that can. A new member goes at the end of its mapping and copies the style of its siblings. Reading the document back gives exactly v.

Set refuses to write below an alias (ErrAliasWrite) and a key that occurs twice (ErrAmbiguousKey). Replacing the alias node itself is allowed. On any error the document is unchanged and the error is an *EditError.

Example

A string that cannot stay plain is quoted, a value added to a mapping goes at its end in the style of its siblings, and removing the last entry of a nested mapping leaves {}.

package main

import (
	"fmt"
	"log"

	oneyaml "oneyaml.dev/go"
)

func main() {
	doc, err := oneyaml.Parse([]byte("a: 'x'\nb:\n  c: 1\n"))
	if err != nil {
		log.Fatal(err)
	}
	if err := doc.Set("/a", "true"); err != nil {
		log.Fatal(err)
	}
	if err := doc.Set("/d", "new"); err != nil {
		log.Fatal(err)
	}
	if err := doc.Remove("/b/c"); err != nil {
		log.Fatal(err)
	}
	fmt.Print(string(doc.Bytes()))
}
Output:
a: 'true'
b: {}
d: 'new'
Example (Alias)

Writing below an alias would change every other place that shares the anchored node, so it is refused and nothing changes.

package main

import (
	"errors"
	"fmt"
	"log"

	oneyaml "oneyaml.dev/go"
)

func main() {
	doc, err := oneyaml.Parse([]byte("base: &b\n  x: 1\ncopy: *b\n"))
	if err != nil {
		log.Fatal(err)
	}
	err = doc.Set("/copy/x", 2)
	fmt.Println(errors.Is(err, oneyaml.ErrAliasWrite))
	err = doc.Set("/base/x", 2)
	fmt.Println(err)
	fmt.Print(string(doc.Bytes()))
}
Output:
true
<nil>
base: &b
  x: 2
copy: *b
Example (AnchorsAndTags)

An anchor stays on a replaced value, so every alias reads the new value. A tag stays when the new value reads back as exactly that value under it, and replacing an alias itself writes the new value in its place.

package main

import (
	"fmt"
	"log"

	oneyaml "oneyaml.dev/go"
)

func main() {
	doc, err := oneyaml.Parse([]byte("a: &x 1\nb: *x\nt: !custom foo\n"))
	if err != nil {
		log.Fatal(err)
	}
	if err := doc.Set("/a", 2); err != nil {
		log.Fatal(err)
	}
	if err := doc.Set("/t", "bar"); err != nil {
		log.Fatal(err)
	}
	fmt.Print(string(doc.Bytes()))

	var b int
	if err := doc.Get("/b", &b); err != nil {
		log.Fatal(err)
	}
	fmt.Println(b)
	if err := doc.Set("/b", 9); err != nil {
		log.Fatal(err)
	}
	fmt.Print(string(doc.Bytes()))
}
Output:
a: &x 2
b: *x
t: !custom bar
2
a: &x 2
b: 9
t: !custom bar
Example (NewKey)

A new key goes at the end of its mapping and copies the style of its siblings. A null that is already there keeps its spelling.

package main

import (
	"fmt"
	"log"

	oneyaml "oneyaml.dev/go"
)

func main() {
	doc, err := oneyaml.Parse([]byte("# ports\nname: 'api'\nport: 80\nnote: ~\n"))
	if err != nil {
		log.Fatal(err)
	}
	if err := doc.Set("/owner", "ops"); err != nil {
		log.Fatal(err)
	}
	if err := doc.Set("/note", nil); err != nil {
		log.Fatal(err)
	}
	if err := doc.Set("/port", 8080); err != nil {
		log.Fatal(err)
	}
	fmt.Print(string(doc.Bytes()))
}
Output:
# ports
name: 'api'
port: 8080
note: ~
owner: 'ops'
Example (StyleFallback)

When the existing style cannot hold the new value, only that value moves to the nearest style that can. A plain scalar stays plain when the Core schema reads it back as the same string, and becomes quoted when it would not.

package main

import (
	"fmt"
	"log"

	oneyaml "oneyaml.dev/go"
)

func main() {
	doc, err := oneyaml.Parse([]byte("a: plain\nb: plain\nc: plain\nd: \"quoted\"\n"))
	if err != nil {
		log.Fatal(err)
	}
	for _, edit := range []struct {
		ptr string
		v   any
	}{
		{"/a", "yes"},         // Core reads this as a string: it stays plain
		{"/b", "true"},        // Core reads this as a boolean: it is quoted
		{"/c", "two\nlines"},  // a line break needs an escape: double quotes
		{"/d", "now: plain?"}, // a double-quoted scalar stays double-quoted
	} {
		if err := doc.Set(edit.ptr, edit.v); err != nil {
			log.Fatal(err)
		}
	}
	fmt.Print(string(doc.Bytes()))
}
Output:
a: yes
b: 'true'
c: "two\nlines"
d: "now: plain?"

func (*Document) TagDirectives

func (d *Document) TagDirectives() []TagDirective

TagDirectives returns the %TAG directives written before the document, in order. Directives belong to one document: those of an earlier document are not included.

Example
package main

import (
	"fmt"
	"log"

	oneyaml "oneyaml.dev/go"
)

func main() {
	doc, err := oneyaml.Parse([]byte("%YAML 1.2\n%TAG !e! tag:example.com,2000:\n---\na: 1\n...\n---\nb: 2\n"))
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(doc.Doc(0).ExplicitStart(), doc.Doc(0).ExplicitEnd(), doc.Doc(0).TagDirectives())
	fmt.Println(doc.Doc(0).Version())
	fmt.Println(doc.Doc(1).ExplicitStart(), doc.Doc(1).ExplicitEnd(), doc.Doc(1).TagDirectives())
	fmt.Println(doc.Doc(1).Version())
}
Output:
true true [{!e! tag:example.com,2000:}]
1 2 true
true false []
0 0 false

func (*Document) Version

func (d *Document) Version() (major, minor int, ok bool)

Version returns the version of a %YAML directive the document carries. ok is false when it has none, in which case the document is read as YAML 1.2.

func (*Document) WriteTo

func (d *Document) WriteTo(w io.Writer) (int64, error)

WriteTo writes the stream to w.

Example

Document writes the bytes it holds to any io.Writer.

package main

import (
	"fmt"
	"log"
	"strings"

	oneyaml "oneyaml.dev/go"
)

func main() {
	doc, err := oneyaml.Parse([]byte("# kept\na: 1\n"))
	if err != nil {
		log.Fatal(err)
	}
	var sb strings.Builder
	n, err := doc.WriteTo(&sb)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Print(n, " ", sb.String())
}
Output:
12 # kept
a: 1

type EditError

type EditError struct {
	// Op is the operation, for example "replace".
	Op string
	// Pointer is the RFC 6901 pointer the operation addressed. For "move" and
	// "copy" it is the destination.
	Pointer string
	// From is the source pointer of a "move" or "copy".
	From string
	// Patch reports that the operation was part of a patch, and Index is its
	// zero-based position in that patch.
	Patch bool
	Index int
	// Kind is one of the sentinel errors above, or another cause.
	Kind error
}

EditError reports a failed document edit or patch operation.

Example

An edit that fails returns an *EditError that says what was attempted.

package main

import (
	"errors"
	"fmt"
	"log"

	oneyaml "oneyaml.dev/go"
)

func main() {
	doc, err := oneyaml.Parse([]byte("a: 1\n"))
	if err != nil {
		log.Fatal(err)
	}
	err = doc.Set("/a/b/c", 2)
	var ee *oneyaml.EditError
	if errors.As(err, &ee) {
		fmt.Println(ee.Op, ee.Pointer, errors.Is(err, oneyaml.ErrNotFound))
	}
}
Output:
set /a/b/c true

func (*EditError) Error

func (e *EditError) Error() string

Error describes the operation that failed and why. The package prefix appears once, and an empty pointer is left out.

func (*EditError) Unwrap

func (e *EditError) Unwrap() error

Unwrap lets errors.Is match Kind.

type Encoder

type Encoder struct {
	// contains filtered or unexported fields
}

An Encoder writes YAML documents to an output stream.

Example

An Encoder writes several documents to one stream, separated by "---".

package main

import (
	"log"
	"os"

	oneyaml "oneyaml.dev/go"
)

func main() {
	enc := oneyaml.NewEncoder(os.Stdout)
	if err := enc.Encode(map[string]any{"kind": "first"}); err != nil {
		log.Fatal(err)
	}
	if err := enc.Encode([]int{1, 2}); err != nil {
		log.Fatal(err)
	}
	if err := enc.Close(); err != nil {
		log.Fatal(err)
	}
}
Output:
kind: first
---
- 1
- 2

func NewEncoder

func NewEncoder(w io.Writer, opts ...Option) *Encoder

NewEncoder returns an Encoder that writes to w.

func (*Encoder) Close

func (e *Encoder) Close() error

Close ends the stream. It does not close the underlying writer. Encode returns an error after Close.

func (*Encoder) Encode

func (e *Encoder) Encode(v any) error

Encode writes v as the next document of the stream. Documents after the first are preceded by a "---" line. Each document is written to the underlying writer with a single Write call, and nothing is written if v cannot be encoded.

type LimitError

type LimitError struct {
	// Limit names the limit, for example "depth".
	Limit string
	// Max is the bound that was exceeded.
	Max int
	// Option names the option that raises the bound.
	Option string
	// Path is the RFC 6901 pointer to the value where the limit was hit, if known.
	Path string
}

LimitError reports that a limit was exceeded.

func (*LimitError) Error

func (e *LimitError) Error() string

Error names the limit, where it was hit and the option that raises it.

type LimitSet

type LimitSet struct {
	// MaxDepth is the deepest nesting of collections.
	MaxDepth int
	// MaxAliasNodes is the number of nodes that alias expansion may produce.
	// Left at its default, the bound is raised to ten times the number of nodes
	// the document itself holds (two times with Untrusted), so a large document
	// that reuses its anchors is not refused. A value you set with Limits
	// applies exactly as given.
	MaxAliasNodes int
	// MaxAliasBytes is the number of scalar bytes that alias expansion may
	// produce. When left at zero it is raised with the input size in the same way
	// as MaxAliasNodes.
	MaxAliasBytes int
	// MaxInputBytes is the largest input accepted.
	MaxInputBytes int
	// MaxScalarBytes is the longest single scalar accepted.
	MaxScalarBytes int
	// MaxDocuments is the most documents accepted in one stream.
	MaxDocuments int
	// MaxNumberDigits is the most digits (counting the whole text of a float)
	// that an integer or float scalar may have when it is converted to a
	// number, whether into a big.Int, a float, or a generic value. Turning a
	// very long decimal into a big.Int takes time that grows with the square of
	// its length, so a few megabytes of digits could otherwise keep a call busy
	// for minutes. A longer number makes the call fail with a *LimitError.
	// Reading the scalar as a string, or keeping its text in a Node or Document,
	// is never limited because no conversion happens.
	MaxNumberDigits int
	// MaxPatchWorkRatio bounds the text that the edits of one patch (ApplyPatch,
	// Apply or ApplyMergePatch) may have to read again, as a multiple of the
	// document plus the patch, with 16 MiB on top. Most edits touch a small
	// region, but some re-read the whole document, so a long patch against a large
	// document could otherwise cost time that grows with the square of its size.
	// A patch that goes past the bound fails with a *LimitError and changes
	// nothing.
	MaxPatchWorkRatio int
}

LimitSet bounds the work and memory a call may spend.

A field left at zero is not changed: it keeps the bound given by the options before Limits (the value DefaultLimits gives it if there are none, which is no bound for the fields that have none by default). So setting one field adds that bound without lifting the others, and Untrusted(), Limits(LimitSet{MaxDepth: 50}) tightens the depth of the untrusted preset. Set a field to NoBound, or any negative value, to remove its bound, or pass NoLimits to remove them all.

MaxDepth and MaxNumberDigits also apply when writing (the latter to numbers a MarshalJSON method returns); the other limits apply to reading.

func DefaultLimits

func DefaultLimits() LimitSet

DefaultLimits returns the limits that apply when no limit option is given: generous bounds that real documents do not reach but that stop alias bombs and runaway nesting.

Example

DefaultLimits applies when no limit option is given. UntrustedLimits is what the Untrusted option applies. A zero field means the bound is not applied.

package main

import (
	"fmt"

	oneyaml "oneyaml.dev/go"
)

func main() {
	fmt.Printf("%+v\n", oneyaml.DefaultLimits())
	fmt.Printf("%+v\n", oneyaml.UntrustedLimits())
}
Output:
{MaxDepth:10000 MaxAliasNodes:1000000 MaxAliasBytes:67108864 MaxInputBytes:0 MaxScalarBytes:0 MaxDocuments:0 MaxNumberDigits:100000 MaxPatchWorkRatio:256}
{MaxDepth:1000 MaxAliasNodes:100000 MaxAliasBytes:8388608 MaxInputBytes:67108864 MaxScalarBytes:16777216 MaxDocuments:10000 MaxNumberDigits:4300 MaxPatchWorkRatio:32}

func UntrustedLimits

func UntrustedLimits() LimitSet

UntrustedLimits returns the tighter limits that the Untrusted option applies.

type Marshaler

type Marshaler interface {
	MarshalYAML() (any, error)
}

Marshaler is implemented by types that choose their own YAML form. The value MarshalYAML returns is written in place of the receiver; it may be any value Marshal accepts, including another Marshaler.

Example
package main

import (
	"fmt"
	"log"

	oneyaml "oneyaml.dev/go"
)

type celsius float64

// MarshalYAML lets a type choose its own YAML form.
func (c celsius) MarshalYAML() (any, error) { return fmt.Sprintf("%.1fC", float64(c)), nil }

func main() {
	out, err := oneyaml.Marshal(map[string]any{"temp": celsius(21.5)})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Print(string(out))
}
Output:
temp: 21.5C

type Node

type Node struct {
	// contains filtered or unexported fields
}

A Node is a read-only view of one node of a Document. It is a small value and is cheap to copy. A Node becomes stale when the Document is edited: Stale then reports true, methods that return an error report ErrStaleNode, and the others return zero values ("", 0, false, an invalid Node). Zero values are easy to write back by mistake, so to edit while reading, collect the pointers first (see Pointer) and edit afterwards, or read the nodes again after each edit.

Example

A Node is a read-only view of the document: kind, style, text, anchors, tags, comments and position.

package main

import (
	"fmt"
	"log"
	"strings"

	oneyaml "oneyaml.dev/go"
)

func main() {
	doc, err := oneyaml.Parse([]byte("# the name\nname: api # service\nitems: [1, two]\n"))
	if err != nil {
		log.Fatal(err)
	}
	root := doc.Root()
	name, _ := root.Lookup("name")
	line, col := name.Position()
	fmt.Println(name.Value(), name.Tag(), name.LineComment(), line, col)
	fmt.Println(strings.TrimSpace(name.HeadComment()))
	items, _ := root.Lookup("items")
	fmt.Println(items.Style() == oneyaml.FlowStyle, items.Len(), items.Item(1).Value())
}
Output:
api tag:yaml.org,2002:str # service 2 7
# the name
true 2 two

func (Node) AliasName

func (n Node) AliasName() string

AliasName returns the anchor an alias node refers to, or "".

func (Node) Anchor

func (n Node) Anchor() string

Anchor returns the anchor name of the node without "&", or "".

func (Node) Decode

func (n Node) Decode(v any) error

Decode reads the node and everything below it into the value v points to, as Document.Decode does for a whole document. A *TypeError's Path is relative to the node. An alias inside the subtree is expanded even when its anchor lies outside it. A Node made stale by an edit reports an *EditError matching ErrStaleNode; the zero Node reports one matching ErrNotFound.

Example

Decode on a node reads that part of the document; an alias inside it is expanded even when its anchor is defined outside.

package main

import (
	"fmt"
	"log"

	oneyaml "oneyaml.dev/go"
)

func main() {
	doc, err := oneyaml.Parse([]byte("defaults: &d {retries: 3}\njob:\n  settings: *d\n"))
	if err != nil {
		log.Fatal(err)
	}
	job, _ := doc.Root().Lookup("job")
	var v map[string]map[string]int
	if err := job.Decode(&v); err != nil {
		log.Fatal(err)
	}
	fmt.Println(v)
}
Output:
map[settings:map[retries:3]]

func (Node) Entries

func (n Node) Entries() iter.Seq2[Node, Node]

Entries returns the key and value of each entry of a mapping in order. It yields nothing for any other node.

The loop may edit the Document, as a loop over a map may change the map. After an edit the nodes already yielded are stale, but the loop goes on: it finds the mapping again by the pointer it had before the latest edit and yields the entry at the next position, as fresh nodes. An edit that inserts or removes items of a sequence on the way to this mapping moves the pointer with it. The loop ends if the mapping is replaced or removed, is no longer a mapping, or has no pointer (see Node.Pointer), and also when more than a few loops over one Document are running at once and this one is not among the first.

Entries added at the end during the loop are yielded, so a body that adds an entry on every pass never ends. Entries added or removed before the current one may make the loop skip entries or see some twice, as Go leaves open for a map: to add or remove while ranging, collect the pointers first (Node.Pointer), finish the loop, then edit.

Example (Edit)

Ranging over a collection and setting each value as the loop goes.

package main

import (
	"fmt"
	"log"

	oneyaml "oneyaml.dev/go"
)

func main() {
	doc, err := oneyaml.Parse([]byte("images:\n  web: nginx:1.25\n  db: postgres:15\n"))
	if err != nil {
		log.Fatal(err)
	}
	imgs, ok := doc.Root().Lookup("images")
	if !ok {
		log.Fatal("no images")
	}
	for k, v := range imgs.Entries() {
		// Each Set makes the nodes read so far stale, but the loop goes on with fresh ones.
		if err := doc.Set("/images/"+k.Value(), v.Value()+"-patched"); err != nil {
			log.Fatal(err)
		}
	}
	fmt.Print(string(doc.Bytes()))
}
Output:
images:
  web: nginx:1.25-patched
  db: postgres:15-patched

func (Node) Entry

func (n Node) Entry(i int) (key, value Node)

Entry returns the key and value of entry i of a mapping. Looping over i is linear in the length; Entries is the shorter way to write that loop.

func (Node) ExplicitTag

func (n Node) ExplicitTag() (tag string, ok bool)

ExplicitTag returns the tag written on the node, resolved: the handle is expanded by the %TAG directives of the node's document, "!<uri>" gives the uri, and ASCII percent escapes are decoded exactly as the parser does (non-ASCII escapes stay as written). The non-specific tag "!" is returned as "!". ok is false when no tag was written, which Tag cannot tell apart from a tag the Core schema supplies.

Example
package main

import (
	"fmt"
	"log"

	oneyaml "oneyaml.dev/go"
)

func main() {
	doc, err := oneyaml.Parse([]byte("%TAG !e! tag:example.com,2000:\n--- !e!point\nx: !!str 1\ny: 2\n"))
	if err != nil {
		log.Fatal(err)
	}
	root := doc.Root()
	tag, ok := root.ExplicitTag()
	fmt.Println(tag, ok, root.TagAsWritten())
	x, _ := root.Lookup("x")
	fmt.Println(x.ExplicitTag())
	y, _ := root.Lookup("y")
	// Tag gives the tag the Core schema assigns; ExplicitTag says none was written.
	fmt.Println(y.Tag())
	fmt.Println(y.ExplicitTag())
}
Output:
tag:example.com,2000:point true !e!point
tag:yaml.org,2002:str true
tag:yaml.org,2002:int
 false

func (Node) HeadComment

func (n Node) HeadComment() string

HeadComment returns the comment lines directly above the entry the node belongs to (no blank line between), each line as written, or "".

func (Node) IsValid

func (n Node) IsValid() bool

IsValid reports whether n refers to a node of its document. It is false for the zero Node and for a stale Node.

func (Node) Item

func (n Node) Item(i int) Node

Item returns item i of a sequence, or an invalid Node. Looping over i is linear in the length; Items is the shorter way to write that loop.

func (Node) Items

func (n Node) Items() iter.Seq[Node]

Items returns the items of a sequence in order. It yields nothing for any other node.

The loop may edit the Document, as a loop over a map may change the map. After an edit the nodes already yielded are stale, but the loop goes on: it finds the sequence again by the pointer it had before the latest edit and yields the item at the next position, as a fresh node. An edit that inserts or removes items of a sequence on the way to this one moves the pointer with it. The loop ends if the sequence is replaced or removed, is no longer a sequence, or has no pointer (see Node.Pointer), and also when more than a few loops over one Document are running at once and this one is not among the first.

Items appended during the loop are yielded, so a body that appends on every pass never ends. Items inserted or removed before the current one may make the loop skip items or see some twice, as Go leaves open for a map: to add or remove while ranging, collect the pointers first (Node.Pointer), finish the loop, then edit.

Example
package main

import (
	"fmt"
	"log"

	oneyaml "oneyaml.dev/go"
)

func main() {
	doc, err := oneyaml.Parse([]byte("- a\n- [b, c]\n- {d: e}\n"))
	if err != nil {
		log.Fatal(err)
	}
	for item := range doc.Root().Items() {
		fmt.Println(item.Kind())
	}
	doc, err = oneyaml.Parse([]byte("x: 1\ny: [2]\n"))
	if err != nil {
		log.Fatal(err)
	}
	for k, v := range doc.Root().Entries() {
		fmt.Println(k.Value(), v.Kind())
	}
}
Output:
scalar
sequence
mapping
x scalar
y sequence

func (Node) Kind

func (n Node) Kind() NodeKind

Kind returns the kind of the node.

func (Node) Len

func (n Node) Len() int

Len returns the number of items of a sequence or entries of a mapping.

func (Node) LineComment

func (n Node) LineComment() string

LineComment returns the comment that follows the node on its line, including the "#", or "".

func (Node) Lookup

func (n Node) Lookup(key string) (Node, bool)

Lookup returns the value of the first entry of a mapping whose key has the scalar text key. Aliases are not followed.

func (Node) Pointer

func (n Node) Pointer() (string, error)

Pointer returns the RFC 6901 pointer of the node within its own document, ready for Document.Set, Get or Remove. It is how to edit while walking: collect the pointers of the nodes you want, finish the loop, then edit (an edit makes every Node read earlier stale).

Pointer returns an error matching ErrStaleNode when the Document has been edited since the node was read, and one matching ErrNotFound for the zero Node. It returns an error matching ErrNoPointer, and never a guess, when no pointer addresses the node: a mapping key (simple or complex), anything inside a complex key, or a node whose pointer would lead to a different node (the key occurs twice in its mapping). An alias node has the pointer of the alias itself; Target gives the node it refers to, whose pointer is the place of the anchor, not a path through the alias.

Example

Collect the pointers of the images, then edit them.

package main

import (
	"errors"
	"fmt"
	"log"

	oneyaml "oneyaml.dev/go"
)

func main() {
	doc, err := oneyaml.Parse([]byte("images:\n  web: nginx:1.25\n  db: postgres:15\n"))
	if err != nil {
		log.Fatal(err)
	}
	imgs, ok := doc.Root().Lookup("images")
	if !ok {
		log.Fatal("no images")
	}

	// First collect where each value is, then edit. This works whatever the
	// edits do, including adding or removing entries.
	var ptrs, vals []string
	for _, v := range imgs.Entries() {
		p, err := v.Pointer()
		if err != nil {
			log.Fatal(err)
		}
		ptrs = append(ptrs, p)
		vals = append(vals, v.Value())
	}
	// Then edit by pointer.
	for i, p := range ptrs {
		if err := doc.Set(p, vals[i]+"-patched"); err != nil {
			log.Fatal(err)
		}
	}
	fmt.Print(string(doc.Bytes()))

	// Nodes read before the edits are stale now.
	_, err = imgs.Pointer()
	fmt.Println(imgs.Stale(), errors.Is(err, oneyaml.ErrStaleNode))
}
Output:
images:
  web: nginx:1.25-patched
  db: postgres:15-patched
true true

func (Node) Position

func (n Node) Position() (line, column int)

Position returns the 1-based line and column of the start of the node, counted in characters.

func (Node) Source

func (n Node) Source() string

Source returns the node's text exactly as written.

func (Node) Span

func (n Node) Span() (start, end int)

Span returns the byte offsets of the node's text in Document.Bytes, without its anchor and tag. The offsets refer to the UTF-8 text of the stream.

func (Node) Stale

func (n Node) Stale() bool

Stale reports whether n came from a Document that has been edited since n was read. A stale Node is not valid: its methods return zero values (or ErrStaleNode where they return an error). The zero Node is not stale, only invalid.

func (Node) Style

func (n Node) Style() NodeStyle

Style returns how the node is written.

func (Node) Tag

func (n Node) Tag() string

Tag returns the resolved tag of the node: the explicit tag with its handle expanded by the %TAG directives of the node's document, or the tag the Core schema gives the node ("tag:yaml.org,2002:str", ...). The non-specific tag "!" gives a scalar the string tag and a collection its default tag, as the specification says (section 6.9.1); the decoder reads the node the same way. An alias has no tag of its own and returns "".

func (Node) TagAsWritten

func (n Node) TagAsWritten() string

TagAsWritten returns the tag exactly as it appears in the text, such as "!!str", "!e!foo" or "!<tag:x,2000:y>", or "" when the node has no tag.

func (Node) Target

func (n Node) Target() Node

Target returns the node an alias refers to; for any other node it returns n.

func (Node) Value

func (n Node) Value() string

Value returns the text of a scalar node: quotes removed, escapes expanded, line folding and block chomping applied. For other nodes it is "".

type NodeKind

type NodeKind uint8

NodeKind is the kind of a Node.

const (
	InvalidNode NodeKind = iota
	ScalarNode
	MappingNode
	SequenceNode
	AliasNode
)

Node kinds. The zero value marks an invalid Node.

func (NodeKind) String

func (k NodeKind) String() string

String returns the kind name: "scalar", "mapping", "sequence", "alias" or "invalid".

type NodeStyle

type NodeStyle uint8

NodeStyle is how a node is written in the source.

const (
	PlainStyle NodeStyle = iota
	SingleQuotedStyle
	DoubleQuotedStyle
	LiteralStyle
	FoldedStyle
	BlockStyle
	FlowStyle
)

Node styles. Plain, SingleQuoted, DoubleQuoted, Literal and Folded describe scalars; BlockStyle and FlowStyle describe collections.

type Option

type Option func(*config)

Option configures Marshal, Unmarshal, an Encoder, a Decoder or Parse. One option type is shared by every entry point; an option that does not apply to the call it is passed to is ignored.

func Indent

func Indent(n int) Option

Indent sets how many spaces each nesting level of block output is indented by. The default is 2. Values below 1 select the default and values above 9 are treated as 9.

Example
package main

import (
	"fmt"
	"log"

	oneyaml "oneyaml.dev/go"
)

func main() {
	out, err := oneyaml.Marshal(map[string]any{"a": map[string]any{"b": []any{1, 2}}}, oneyaml.Indent(4))
	if err != nil {
		log.Fatal(err)
	}
	fmt.Print(string(out))
}
Output:
a:
    b:
        - 1
        - 2

func KnownFields

func KnownFields() Option

KnownFields makes reading into a struct fail on a mapping key that matches no field. It applies to reads and is ignored when writing.

Example
var cfg struct {
	Name string `yaml:"name"`
}
err := oneyaml.Unmarshal([]byte("name: api\nnmae: typo\n"), &cfg, oneyaml.KnownFields())
var te *oneyaml.TypeError
if errors.As(err, &te) {
	fmt.Println(te.Path, te.Line, te.Column)
}
Output:
/nmae 2 1

func Limits

func Limits(l LimitSet) Option

Limits sets the bounds that l names and leaves the others as the options before it set them: the defaults if there are none, the bounds of Untrusted after Untrusted, no bounds after NoLimits. So Untrusted(), Limits(LimitSet{MaxDepth: 50}) keeps every untrusted bound and tightens the depth, and NoLimits(), Limits(LimitSet{MaxInputBytes: 1 << 20}) bounds the input and nothing else.

A positive field sets that bound; NoBound removes it. A bound you set applies exactly as given, while an alias bound you leave alone keeps the behaviour it had: the default and Untrusted ones still grow with the size of the document. Limits(LimitSet{}) changes nothing. When writing, MaxDepth and MaxNumberDigits are used.

Example
bomb := "a: &a [x, x, x, x]\nb: &b [*a, *a, *a, *a]\nc: [*b, *b, *b, *b]\n"
var v any
err := oneyaml.Unmarshal([]byte(bomb), &v, oneyaml.Limits(oneyaml.LimitSet{MaxAliasNodes: 20}))
var le *oneyaml.LimitError
if errors.As(err, &le) {
	fmt.Println(le.Limit, le.Max)
}
Output:
alias expansion nodes 20
Example (AddBound)

A LimitSet passed to Limits changes only the fields that are set: here the input size gains a bound, and the default depth bound stays.

package main

import (
	"errors"
	"fmt"
	"strings"

	oneyaml "oneyaml.dev/go"
)

func main() {
	opt := oneyaml.Limits(oneyaml.LimitSet{MaxInputBytes: 1 << 20})
	large := []byte("a: " + strings.Repeat("x", 2<<20) + "\n")
	deep := []byte(strings.Repeat("[", 10001) + strings.Repeat("]", 10001))
	var v any
	var le *oneyaml.LimitError

	err := oneyaml.Unmarshal(large, &v, opt)
	if errors.As(err, &le) {
		fmt.Println(le.Limit, le.Max)
	}
	err = oneyaml.Unmarshal(deep, &v, opt)
	if errors.As(err, &le) {
		fmt.Println(le.Limit, le.Max)
	}
}
Output:
input size 1048576
depth 10000

func MergeKeys

func MergeKeys() Option

MergeKeys makes reading treat the "<<" key as a YAML merge key instead of an ordinary key. It applies to reads and is ignored when writing.

Example
src := []byte("base: &base {x: 1, y: 2}\nchild:\n  <<: *base\n  y: 3\n")
var v map[string]map[string]int
err := oneyaml.Unmarshal(src, &v, oneyaml.MergeKeys())
fmt.Println(v["child"], err)
Output:
map[x:1 y:3] <nil>

func NoLimits

func NoLimits() Option

NoLimits removes every limit, including the depth limit when writing. Only use it for input you trust. A Limits option after it adds back just the bounds it names.

Example

NoLimits removes every bound, including the depth limit. Use it only for input you trust.

package main

import (
	"errors"
	"fmt"
	"strings"

	oneyaml "oneyaml.dev/go"
)

func main() {
	deep := strings.Repeat("[", 10001) + strings.Repeat("]", 10001)
	var v any

	err := oneyaml.Unmarshal([]byte(deep), &v)
	var le *oneyaml.LimitError
	fmt.Println(errors.As(err, &le))
	fmt.Println(oneyaml.Unmarshal([]byte(deep), &v, oneyaml.NoLimits()))
}
Output:
true
<nil>

func Untrusted

func Untrusted() Option

Untrusted selects UntrustedLimits, for input from a source you do not control. It replaces any limits set before it. Options after it can tighten or lift single bounds: Untrusted(), Limits(LimitSet{MaxDepth: 50}).

Example

Untrusted tightens every limit, for input from a source you do not control. A limit that is hit is a *LimitError that names the option which raises it.

package main

import (
	"errors"
	"fmt"
	"strings"

	oneyaml "oneyaml.dev/go"
)

func main() {
	deep := strings.Repeat("[", 2000) + strings.Repeat("]", 2000)
	var v any

	// The default depth limit lets this through; Untrusted does not.
	fmt.Println(oneyaml.Unmarshal([]byte(deep), &v))
	err := oneyaml.Unmarshal([]byte(deep), &v, oneyaml.Untrusted())
	var le *oneyaml.LimitError
	if errors.As(err, &le) {
		fmt.Println(le.Limit, le.Max, le.Option)
	}
}
Output:
<nil>
depth 1000 Limits
Example (Tighten)

A Limits option after Untrusted changes only the bounds it names: here depth is tightened, and the untrusted bound on the digits of a number still holds.

package main

import (
	"errors"
	"fmt"
	"strings"

	oneyaml "oneyaml.dev/go"
)

func main() {
	opts := []oneyaml.Option{oneyaml.Untrusted(), oneyaml.Limits(oneyaml.LimitSet{MaxDepth: 50})}
	deep := strings.Repeat("[", 100) + strings.Repeat("]", 100)
	long := strings.Repeat("9", 5000)
	var v any
	var le *oneyaml.LimitError

	for _, in := range []string{deep, long} {
		err := oneyaml.Unmarshal([]byte(in), &v, opts...)
		if errors.As(err, &le) {
			fmt.Println(le.Limit, le.Max)
		}
	}
}
Output:
depth 50
number digits 4300

type PatchOp

type PatchOp struct {
	// Op is "add", "remove", "replace", "move", "copy" or "test".
	Op string
	// Path is the RFC 6901 pointer the operation works on.
	Path string
	// From is the source pointer of "move" and "copy".
	From string
	// Value is the operand of "add", "replace" and "test": anything Marshal
	// accepts, with nil meaning null.
	Value any
}

A PatchOp is one RFC 6902 operation for Apply.

func PatchAdd

func PatchAdd(path string, v any) PatchOp

PatchAdd returns an "add" operation.

func PatchCopy

func PatchCopy(from, path string) PatchOp

PatchCopy returns a "copy" operation.

func PatchMove

func PatchMove(from, path string) PatchOp

PatchMove returns a "move" operation.

func PatchRemove

func PatchRemove(path string) PatchOp

PatchRemove returns a "remove" operation.

func PatchReplace

func PatchReplace(path string, v any) PatchOp

PatchReplace returns a "replace" operation.

func PatchTest

func PatchTest(path string, v any) PatchOp

PatchTest returns a "test" operation.

type SyntaxError

type SyntaxError struct {
	// Line and Column are 1-based, counted in characters; Offset is the byte offset.
	Line, Column, Offset int
	// Category is a short stable name for the kind of error.
	Category string
	// Msg describes the problem.
	Msg string
}

SyntaxError reports YAML that is not well formed.

Example

A *SyntaxError carries the position and a stable category.

package main

import (
	"errors"
	"fmt"

	oneyaml "oneyaml.dev/go"
)

func main() {
	err := oneyaml.Unmarshal([]byte("a: 1\na: 2\n"), new(any))
	var se *oneyaml.SyntaxError
	if errors.As(err, &se) {
		fmt.Println(se.Category, se.Line, se.Column)
	}
}
Output:
duplicate-key 2 1

func (*SyntaxError) Error

func (e *SyntaxError) Error() string

Error returns the message with its line and column.

type TagDirective

type TagDirective struct {
	// Handle is the tag handle as written, such as "!" or "!e!".
	Handle string
	// Prefix is the tag prefix as written.
	Prefix string
}

A TagDirective is a %TAG directive: tags written with Handle in the document stand for Prefix followed by the rest of the tag.

type TypeError

type TypeError struct {
	// Path is the RFC 6901 pointer to the value, empty for the root.
	Path string
	// Line and Column locate the value in the input; zero when writing.
	Line, Column int
	// GoType is the Go type involved.
	GoType string
	// Msg describes the problem.
	Msg string
	// Err is the error a MarshalYAML or MarshalText method returned, if that was the cause.
	Err error
}

TypeError reports a value that cannot be converted: a YAML value that does not fit its Go target when reading, or a Go value with no YAML form when writing.

func (*TypeError) Error

func (e *TypeError) Error() string

Error returns the message with the position and path of the value.

func (*TypeError) Unwrap

func (e *TypeError) Unwrap() error

Unwrap returns the error from a user hook, if there was one.

type Unmarshaler

type Unmarshaler interface {
	UnmarshalYAML(node Node) error
}

Unmarshaler is implemented by types that read their own YAML. Unmarshal, Decoder.Decode, Document.Decode and Node.Decode call UnmarshalYAML with a view of the node that holds the value, instead of decoding into the type's fields.

The Node is read-only. Node.Decode on it decodes the subtree into any target, with the same rules as Unmarshal. When the value comes from Unmarshal or a Decoder there is no Document, so the Node covers only that subtree and is valid only during the call; aliases inside it still resolve. A null value leaves a pointer target nil without calling the method. An error the method returns comes back from the decode as a *TypeError whose Unwrap gives it.

Example
package main

import (
	"fmt"
	"log"

	oneyaml "oneyaml.dev/go"
)

// Port reads itself from a node: a bare number, or a mapping with a number.
type Port int

func (p *Port) UnmarshalYAML(n oneyaml.Node) error {
	if n.Kind() == oneyaml.MappingNode {
		var m struct {
			Number int `yaml:"number"`
		}
		if err := n.Decode(&m); err != nil {
			return err
		}
		*p = Port(m.Number)
		return nil
	}
	var v int
	if err := n.Decode(&v); err != nil {
		return err
	}
	*p = Port(v)
	return nil
}

func main() {
	var cfg struct {
		Ports []Port `yaml:"ports"`
	}
	if err := oneyaml.Unmarshal([]byte("ports: [80, {number: 443}]\n"), &cfg); err != nil {
		log.Fatal(err)
	}
	fmt.Println(cfg.Ports)
}
Output:
[80 443]
Example (FromYAMLv3)
package main

import (
	"fmt"
	"log"

	oneyaml "oneyaml.dev/go"
	yaml "oneyaml.dev/go/compat/yamlv3"
)

// legacySize was written for yaml.v3: its UnmarshalYAML takes a *yaml.Node.
// The oneyaml package does not call that form; it reports it, so the type is
// never filled by its fields without a word.
type legacySize struct{ Bytes int }

func (s *legacySize) UnmarshalYAML(n *yaml.Node) error { return n.Decode(&s.Bytes) }

// Size is the same type moved to the oneyaml package: the method takes a
// oneyaml.Node, which has the same Decode method.
type Size struct{ Bytes int }

func (s *Size) UnmarshalYAML(n oneyaml.Node) error { return n.Decode(&s.Bytes) }

func main() {
	src := []byte("limit: 512\n")

	var before struct {
		Limit legacySize `yaml:"limit"`
	}
	err := oneyaml.Unmarshal(src, &before)
	fmt.Println(err)

	var after struct {
		Limit Size `yaml:"limit"`
	}
	if err := oneyaml.Unmarshal(src, &after); err != nil {
		log.Fatal(err)
	}
	fmt.Println(after.Limit.Bytes)
}
Output:
oneyaml: line 1, column 8: /limit: oneyaml_test.legacySize has an UnmarshalYAML method that is neither UnmarshalYAML(oneyaml.Node) error nor UnmarshalYAML(func(any) error) error; change its signature, or remove it to fill the value by its fields
512

Directories

Path Synopsis
compat
k8syaml
Package yaml is a compatibility adapter for sigs.k8s.io/yaml (and its parent ghodss/yaml) built on oneyaml.
Package yaml is a compatibility adapter for sigs.k8s.io/yaml (and its parent ghodss/yaml) built on oneyaml.
yamlv3
Package yaml is a compatibility adapter for gopkg.in/yaml.v3 (go.yaml.in/yaml/v3) built on the oneyaml parser and binder.
Package yaml is a compatibility adapter for gopkg.in/yaml.v3 (go.yaml.in/yaml/v3) built on the oneyaml parser and binder.
internal
bind
Package bind decodes a stream of parse events into Go values.
Package bind decodes a stream of parse events into Go values.
compatopt
Package compatopt carries the hidden settings between the root package and the compat packages.
Package compatopt carries the hidden settings between the root package and the compat packages.
cst
Package cst builds a flat, lossless tree from parse events.
Package cst builds a flat, lossless tree from parse events.
edit
Package edit plans minimal-diff edits of a lossless tree.
Package edit plans minimal-diff edits of a lossless tree.
emit
Package emit writes Go values as YAML text.
Package emit writes Go values as YAML text.
encoding
Package encoding detects and converts the character encodings a YAML stream may use: UTF-8, UTF-16 and UTF-32, each in both byte orders.
Package encoding detects and converts the character encodings a YAML stream may use: UTF-8, UTF-16 and UTF-32, each in both byte orders.
kitcase
Package kitcase reads the parts of a shared conformance case directory that every test runner needs in the same way: the input bytes, which may be stored in a file or described by a recipe, and the "slow" tag.
Package kitcase reads the parts of a shared conformance case directory that every test runner needs in the same way: the input bytes, which may be stored in a file or described by a recipe, and the "slow" tag.
parse
Package parse turns scan tokens into YAML events with a pull API.
Package parse turns scan tokens into YAML events with a pull API.
patch
Package patch holds the parts of RFC 6902 and RFC 7396 patches that do not depend on the document: reading a JSON patch, comparing a node with a patch value under the data model, and copying a value so that it can be written elsewhere in the same document.
Package patch holds the parts of RFC 6902 and RFC 7396 patches that do not depend on the document: reading a JSON patch, comparing a node with a patch value under the data model, and copying a value so that it can be written elsewhere in the same document.
pointer
Package pointer parses RFC 6901 JSON Pointers.
Package pointer parses RFC 6901 JSON Pointers.
resolve
Package resolve decides what a plain scalar means.
Package resolve decides what a plain scalar means.
scan
Package scan is a lossless YAML 1.2 scanner.
Package scan is a lossless YAML 1.2 scanner.
typeinfo
Package typeinfo builds and caches, per Go type, what the encoder and the decoder need to know about it: struct fields with their YAML names and options, embedded and inline fields, and which hooks the type implements.
Package typeinfo builds and caches, per Go type, what the encoder and the decoder need to know about it: struct fields with their YAML names and options, embedded and inline fields, and which hooks the type implements.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL