Recursive AI

Changelog

All oneyaml Go docs

All notable changes are listed here, newest first. The format follows Keep a Changelog, and versions follow Semantic Versioning. Breaking changes are listed first, under Breaking, each with what to change in your code (releases up to 0.3.0 mark them Breaking instead). A new major version has a new module path (oneyaml.dev/go/v2), and the entry says so. What a release promises, and the supported Go versions, are under Stability.

Unreleased

1.0.0 - 2026-10-16

The first stable release. From 1.0.0 the API is stable under the promise in Stability.

Breaking

  • LimitError.Limit is now of the new type Limit and names the LimitSet field of the bound (LimitMaxDepth, LimitMaxAliasNodes and so on). It used to be free text such as "depth", "alias expansion nodes" or "patch output size". The text is now in the new field What. Patch limits report the field that governs them: MaxInputBytes for the patch text and for the document a patch makes, and MaxAliasBytes for the text a patch copies. Migrate: compare le.Limit with the Limit constants, or match a sentinel such as errors.Is(err, oneyaml.ErrMaxDepth), and read le.What where you printed le.Limit.

  • LimitError.Option now has one form, Limits (MaxDepth), which names the option and the LimitSet field that raise the bound. It used to be Limits, Limits or NoLimits, Limits (MaxAliasNodes) or none: split the stream, depending on where the limit was hit. It is empty for the three bounds no option changes: the nesting of a patch value (10,000 levels), the nesting of typed Go values (100,000 levels) and the 2 GiB size of a Document. It is also always empty in compat/yamlv3 and compat/k8syaml, which take no limit options and sometimes suggested one. Migrate: switch on le.Limit rather than reading Option.

  • SyntaxError.Category is now of the new type Category, and CategorySyntax, CategoryEncoding and CategoryDuplicateKey are typed constants. They used to be untyped strings. Comparisons, switch statements and printing still compile. Migrate: where you store a category in a string or pass it as one, write string(se.Category).

  • TypeError.GoType is now a reflect.Type. It used to be a string. It is nil when no type was involved, so two types with one name in different packages can be told apart and a type can be compared: te.GoType == reflect.TypeFor[time.Duration](). Messages print the type as before. Migrate: where you used the string, call te.GoType.String() after checking for nil.

  • KnownFields, MergeKeys and YAML11Strings are now func(...bool) Option, and Document.Get, Document.Decode and Node.Decode now take ...Option (see Added). Every call that compiled before still compiles. A method value or a function-typed variable no longer matches, such as var f func(string, any) error = doc.Get, and neither does an interface that lists the old methods. Migrate: wrap the call in a function literal, such as func(p string, v any) error { return doc.Get(p, v) }.

  • Node.Find now returns an error matching ErrAmbiguousKey for a key written more than once in its mapping, and Node.Lookup now returns false for it. Both used to return the first entry. Document.Get and every edit already refused a pointer through such a key. Migrate: read every entry with Entries, and use Find to learn why a lookup failed (Reading while you edit).

  • Under a MaxInputBytes bound (Untrusted() sets one), Set, Rename, SetHeadComment and SetLineComment now refuse an edit that would make the text longer than the bound. Before, only ApplyPatch and ApplyMergePatch were held to it. The error is an *EditError, and errors.As finds the *LimitError inside it. The document is unchanged, and an edit that makes the text shorter is always allowed. For a UTF-16 or UTF-32 document, these edits and the patches now count the size in the encoding the document is written in, byte order mark included; patches used to count the UTF-8 size. Even with no bound set, an edit or patch is refused when the text would pass 2 GiB, the most a Document can address. Migrate: pass a larger bound when you parse, with Limits(oneyaml.LimitSet{MaxInputBytes: n}) after Untrusted() (Limits).

  • Parse, Unmarshal, the Decoder and the compat packages now refuse two kinds of invalid YAML with a *SyntaxError. Version 0.3.0 loaded both:

    • Inside a flow collection, a flow collection that touches the : of a plain or empty key, as in {a:{}}, [a:[]] and {:{0}}. After a quoted key ({"a":{}}) it is still allowed.
    • A verbatim tag that is neither a local tag nor a URI that starts with a scheme, as in !<!>, !<a> and !<$:?>.

    Migrate: put a space after the colon ({a: {}}), and write such a tag as a local tag (!<!name>), a full URI (!<tag:example.com,2026:name>) or a shorthand tag (!name).

  • compat/yamlv3: Marshal now returns an error naming the type and the method for a MarshalYAML method that is not MarshalYAML() (interface{}, error). It used to skip the method and write the value field by field, as yaml.v3 does. The oneyaml package already refused such a method. Migrate: give the method the signature MarshalYAML() (interface{}, error), or remove it.

  • compat/yamlv3: Marshal now returns an error naming the type for a value of the Node type of gopkg.in/yaml.v3 or go.yaml.in/yaml/v3. This covers such a value given directly, held in a field, or returned by a MarshalYAML method. The adapter used to write the node's Go fields (kind: 8, style: 0, ...), where yaml.v3 writes the YAML the node holds. Migrate: build the adapter's own yaml.Node, or write that value with yaml.v3 (Migrating from yaml.v3).

  • compat/yamlv3: Unmarshal and Decode now return an error naming the type when the target is the Node type of gopkg.in/yaml.v3 or go.yaml.in/yaml/v3, held directly, in a field, in a slice or map, or behind a pointer. They used to return no error and leave the node empty. A null value still leaves a pointer to it nil. Migrate: decode into the adapter's yaml.Node.

Added

  • Sentinel errors for every error kind, so a single errors.Is call tells kinds apart. *SyntaxError matches ErrSyntax, and also ErrEncoding or ErrDuplicateKey for those categories. *LimitError matches ErrLimit, and also the sentinel of its limit, named like the LimitSet field: ErrMaxDepth, ErrMaxAliasNodes, ErrMaxAliasBytes, ErrMaxInputBytes, ErrMaxScalarBytes, ErrMaxDocuments, ErrMaxNumberDigits and ErrMaxPatchWorkRatio, also through an *EditError that wraps it (Limits).
  • The type Limit and its constants LimitMaxDepth, LimitMaxAliasNodes, LimitMaxAliasBytes, LimitMaxInputBytes, LimitMaxScalarBytes, LimitMaxDocuments, LimitMaxNumberDigits and LimitMaxPatchWorkRatio, for LimitError.Limit. Also the field LimitError.What, and the type Category for SyntaxError.Category (see Breaking).
  • oneyaml.Pointer(tokens...) builds an RFC 6901 pointer from keys and indices, escaping ~ and / in each. For example, Pointer("metadata", "labels", "app.kubernetes.io/name") is /metadata/labels/app.kubernetes.io~1name. Use it instead of joining keys with "/".
  • Document.Get, Document.Decode and Node.Decode take options for one read, applied on top of the options the Document was parsed with. For example, doc.Get("/spec", &spec, oneyaml.KnownFields()) reads one part strictly. KnownFields, MergeKeys, Limits, Untrusted and NoLimits apply to a read; other options are ignored. The options also hold for the decodes an UnmarshalYAML hook makes during that read. A bound given to a read of a Document replaces the Document's bound in both directions, so NoLimits() on a read of a Document parsed with Untrusted() lifts its bounds for that read. Pass such options only for input you trust (Reading values back). Inside an UnmarshalYAML hook it is different: options given to Node.Decode may only tighten the limits of the read that called the hook, never loosen them. For each bound the stricter of the caller's and the option's applies, so a type's hook cannot undo a caller's Untrusted(). KnownFields and MergeKeys given there apply as given.
  • KnownFields, MergeKeys and YAML11Strings take an optional bool, so KnownFields(false) turns the option off again, and a later option overrides an earlier one. Calls without an argument behave as before.
  • Node is now documented as comparable: two Nodes are equal when they are the same node of the same state of the same Document. A Node read before an edit is never equal to the one read after it.

Changed

  • LimitError.Path of a limit hit by a decode inside an UnmarshalYAML hook is now the pointer in the whole document, like the path of a *TypeError. This holds when the hook wraps the error, as with fmt.Errorf("...: %w", err) or errors.Join, when hooks are nested and each one wraps it, and when an outer hook decoded a child of its Node. It used to be empty. The read does not change the error the hook returned: errors.As on the read's error finds a copy that carries the whole-document path. A hook may therefore keep that error and return it from later reads, even on several goroutines at once, and those reads return it as it is.
  • A type error found by a decode inside an UnmarshalYAML hook (a Node.Decode, or the function a func-style hook is given) is now reported where the value is in the input, with every mismatch the hook's decode found, as for a read without the hook. Its path is the hook's path followed by the path inside the node the hook decoded, so a hook that decodes a child node reports that child's path. Before, only the first mismatch was reported, at the first key of the hooked mapping, and a child's path was cut off at the hook. A bad value on line 7 could be reported on line 5. This holds at any depth of nested hooks and on every read path.
  • The Unmarshaler documentation now says how a Node that UnmarshalYAML keeps may be used after the method returns: later, from one goroutine at a time, but not from another goroutine at the moment the method returns. A kept Node goes on reading its own subtree, whichever read gave it. One kept from Unmarshal reads the byte slice you passed in, so leave that slice unchanged while you keep the Node. One kept from a Document read becomes stale when the Document is edited.
  • An error that a hook returns from reading other text, such as an Unmarshal of its own input, is now reported at the hook's value with the hook's type as GoType. The position and path inside the other text stay in the message: line 1, column 4: /a: line 8, column 4: /x: .... Before, its path was joined onto the hook's (/a/x) and its own line and column were lost.
  • A type whose UnmarshalYAML method has a form that cannot be called, and a Node used as a decode target, are now collected like a value that does not fit. The value is skipped, the rest of the document is read, and the *TypeError lists every mismatch. The first such value used to stop the read.
  • LimitError.Path is now set for limits that are hit at a value while it is read: alias expansion, scalar size, number digits and the fixed nesting bound of typed values. It was already set for patches. It stays empty for the limits Parse reports and for input size, document count and nesting that the parser stops at. LimitError.Error shortens a Path longer than 512 bytes, as TypeError.Error does.
  • Limit error messages name the option in its new form, or none (see Breaking), and give the path when there is one. The document-count limit from Parse now says "documents per stream", as the Decoder's does.

Fixed

  • An alias refers to the latest anchor of its name written before it, even when that anchor is inside an earlier node with the same anchor name. In a: &b {s: &b 1, t: *b}, t is 1. This document used to be refused with "refers to a node that contains it". In a: &b {se: &b 1} followed by copy: *b, copy is 1; it used to be {se: 1}. Every reader agrees, the compat packages included.
  • An unknown field found under KnownFields() by a decode inside an UnmarshalYAML hook now matches ErrUnknownField. It did not.
  • Edits to a later document of a stream that starts with a byte order mark: Set of a new key and SetHeadComment on its first entry or root work, and the mark stays at the start of the line. They used to fail with "edit could not be applied" or ErrUnsupportedEdit.
  • Set of the empty value of an explicit key (? key) followed by a : value line that belongs to an outer mapping writes the value under the key. It used to fail with "edit could not be applied".
  • compat/yamlv3 reads comments onto the same nodes as yaml.v3 in more layouts:
    • A comment on a dash line with no value (- # note) goes to the first key or scalar of the item, or to a flow collection that comes first in it. It used to be moved above the item.
    • The comments of an empty last item (- # note at the end of the text) are the document's foot comment. They used to be dropped.
    • A foot comment of the item before such a dash line stays with that item. It used to be dropped.
    • A comment line just before the closing bracket of a flow collection is the foot comment of the last item or key. It used to be dropped.
    • The comments after a flow collection that ends on the first line of the text ([a, b] or a: [1, 2], then # note) are the document's foot comment. They used to go to the collection, or to its key.
  • compat/yamlv3 writes text that reads back:
    • A folded scalar (>) always reads back as the value written. Some values used to read back with a line break turned into a space or an extra line break, such as a line followed by an indented line. In some of these cases yaml.v3's own text does not read back.
    • A foot comment held by a block collection that is an item of a sequence is written once, after that collection. It used to be dropped.
    • A blank line between the lines of a head comment is written without indentation, as yaml.v3 writes it.
  • compat/yamlv3 reads each comment line once, and tells one line from another by where it stands in the text, not by its words, so two comments with the same words are both kept. When several sequences start on one dash line (- # a, then - # b, then a mapping), the comment lines above the dashes were given to more than one node, and writing the result repeated them, adding a copy on every round trip. Comments at the end of a document are no longer dropped when the key that would take them has a foot comment already, and a comment block under key: followed by a blank line and another block heads the first key of the value, as in yaml.v3.

0.3.0 - 2026-10-16

Added

  • Node.Find(key) is the checked form of Lookup: it returns the value with a nil error, or an error that says why there is none. The error matches ErrNotFound for an absent key (or a node that is not a mapping), ErrStaleNode for a node read before an edit, and ErrAmbiguousKey when a merge brings the key in from two places of one source, and it is a *LimitError when a merge search was refused by MaxAliasNodes. Lookup reports all of these as false. See Reading while you edit.
  • Node.Err says why a Node reads as zero values: it is nil for a node you can read, matches ErrStaleNode when the document was edited after the node was read, and matches ErrNotFound for the zero Node, such as the one a failed Lookup returns. Value of a stale node is "", the same as an empty scalar, so Err is the check to make. See Reading while you edit.
  • With MergeKeys(), a pointer also finds the keys a << merge brings into a mapping, with the precedence decoding gives them: Document.Get, the patch test operation, the from of a patch copy, Node.Lookup and Node.Find read them. A copy from such a key copies the merged value and leaves the merge source unchanged. Set on such a key adds an explicit key that overrides the merged one. Each merge source is searched once, and the keys read count against MaxAliasNodes. See Merge keys and the Changed entry below.
  • ApplyMergePatch also reads a merge patch written in YAML, with the same refusals as a YAML ApplyPatch. Text that starts with { or [, or is valid JSON, is still read as JSON. A mapping key that is not a string (1: 5, true: 5, an empty key) is refused at any depth with its line and column, because merge patch members are named by strings; quote the name to use it. See Patches written in YAML.

Changed

  • Breaking: more repeated mapping keys are duplicate-key errors, so some input that used to load is refused:

    • repeated .nan keys, in any spelling (.nan and .NaN are one key), read into any, a map[float64]T or another map whose keys are not strings. A map with string keys still compares keys as text;
    • a repeated key that no field of a struct target has, and a key repeated inside a value the target ignores. a: 1 twice into a struct with no field a used to load;
    • compat/yamlv3 reports a key repeated at the top of a mapping its target ignores, as yaml.v3 does. This removes a listed gap.

    Fix such input by removing the repeated key. The rules are in the Unmarshal reference.

  • Breaking: Parse refuses a \u or \U escape that is not a Unicode scalar value, such as a lone surrogate ("\ud83d"), a low surrogate before a high one, or a value past U+10FFFF, with a *SyntaxError. Parse used to accept such a document, and only Get or Decode of the value failed. Unmarshal already refused it. Write a valid escape instead: a character outside the Basic Multilingual Plane as an escaped surrogate pair ("\ud83d\ude00"), which is now read as one character (see Fixed), or as \U0001F600. A lone surrogate is not a character, so replace it, with \uFFFD for example, or remove it.

  • Breaking: with MergeKeys(), an edit that would change a key only a merge source has is refused with ErrAliasWrite, because it would write into the source through an alias: Remove, the patch operations remove, replace and move, a merge patch member null that would delete it, and any write below such a key or through the merge key itself (/use/<</x). These used to fail with ErrNotFound, and the merge patch used to succeed without changing anything. Code that checks errors.Is(err, oneyaml.ErrNotFound) for these edits must check ErrAliasWrite too. To change such a key, edit it in the merge source, where the change reaches every mapping that merges it, or Set an explicit key in the mapping, which overrides the merged one. Without MergeKeys() nothing changes. See Merge keys.

  • Breaking: a plain integer or float written as a mapping key in a part of the document the target ignores is held to MaxNumberDigits, as a key that is read is, because such keys are compared as numbers to find repeated keys. A key with more digits than the bound, such as a very long integer key in a mapping read into a struct that has no field for it, used to load and is now a *LimitError. Values the target ignores are not limited, and neither are quoted keys. If your input has such keys, quote them, or raise the bound with Limits(oneyaml.LimitSet{MaxNumberDigits: n}). See Limits.

  • Breaking: compat/yamlv3 returns an error for a type whose UnmarshalYAML method it cannot call, such as a type from a module that still imports gopkg.in/yaml.v3 and takes that package's *yaml.Node. The error names the type and the method. Such a type used to be filled field by field, which skipped whatever its method checked or converted, as yaml.v3 itself skips a method it does not recognise; the adapter now differs from yaml.v3 here on purpose. Read such a type through a wrapper type of your own whose UnmarshalYAML takes the adapter's own *yaml.Node, or decode that part with yaml.v3. MarshalYAML() (interface{}, error) has the same shape in both packages and keeps working. See Migrating from yaml.v3.

  • Under a MaxInputBytes limit (Untrusted() sets one), the text of a patch or merge patch may be at most an eighth of it, though never less than 64 KiB and never more than the limit. Any patch text used to be read in full, into a list of operations that takes far more memory than the text. See Bounds.

  • With MergeKeys(), a search through merge sources that reads more keys than MaxAliasNodes allows fails with a *LimitError, whether it serves Get, Node.Find, a patch or an edit. Node.Lookup reports it as false, like an absent key; use Node.Find to tell the two apart. Adding a new key with Set does not search merge sources.

  • A map key that does not fit the key type is collected like a value that does not fit: its entry is skipped and the read goes on, instead of stopping at the first such key.

  • A json.Unmarshaler receives the numbers inside a mapping or sequence as written, as it already did for a number on its own, so 3.14159265358979323846 is no longer rounded to a float64 on the way.

  • The integer key 0x10 followed by 16 is a duplicate-key *SyntaxError that names both keys, as 16 followed by 0x10 already was. It used to be a *TypeError saying the two numbers round to the same float64.

  • A syntax error after a value that does not fit its target, such as the stray } in {a: x}} read into an int field, is reported as the *SyntaxError it is. It used to be hidden behind the *TypeError for the value.

  • A key written as an alias that repeats an earlier key is reported at the alias, and the message says so. It used to be reported at the anchor.

  • The message for an unclosed [ or { says where it was opened, as the message for an unclosed quoted scalar does, and the one for a quoted scalar that runs into the next line says which quote to add.

  • Rename, SetHeadComment and SetLineComment parse again only the entries around the edit where they can, as Set does, instead of the whole document; inside a flow collection, for one, the whole document is still parsed again. A range loop that edits as it goes keeps its place instead of counting again from the first item, a run of Set calls through a wide mapping no longer slows down with every call, renames in one mapping keep its index of keys, and a removal of many items in one pass holds one copy of the document's index. These change the work and memory an edit takes, not its result. Benchmark results come in a later update.

Fixed

  • An escaped UTF-16 surrogate pair in a double-quoted scalar, as JSON writes characters outside the Basic Multilingual Plane, is one character: "\ud83d\ude00" reads as 😀. It used to be refused as an escape that is not a Unicode scalar value.
  • compat/yamlv3 places head, line and foot comments on the same nodes as yaml.v3 in many more layouts: a comment after ---, after a lone dash, after an anchor or tag on a collection, or after an explicit key (? k # c), and comment lines between a key and its value. These comments used to be dropped. A value's head comment is written below its line, at the column of its key, as yaml.v3 writes it. See Migrating from yaml.v3 for the layouts that still differ.
  • compat/yamlv3 writes the comments inside a flow collection back where yaml.v3 writes them, and keeps the foot comment of a root scalar or a root flow collection. Both used to be read and then dropped on writing. A comment line just before the closing bracket of a flow collection is still not read.
  • Adding an entry after a block scalar whose last line starts with a tab, at the end of the input, no longer changes that scalar's value: the header now strips the final line break, so the scalar reads back as it was.
  • compat/yamlv3 writes an alias used as a mapping key with a space before the colon (*a : 1), so the output reads back. Without it, a YAML 1.2 reader takes *a: as an alias named a:.

0.2.0 - 2026-10-16

Added

  • Document.Rename changes the key of a mapping entry in place, keeping the value, its position and its comments. It refuses a key that already exists (ErrKeyExists), a pointer that is not a mapping entry (ErrNotAnEntry), and a key whose anchor an alias uses (ErrAnchorInUse). See Renaming a key.
  • Document.SetHeadComment and Document.SetLineComment write, replace or remove the comment above an entry or after a value. Text that cannot be a comment is refused with ErrInvalidComment. See Comments.
  • ApplyPatch also reads a patch written as a YAML block list of operations. Text that starts with [ or { is still read as JSON only. A YAML patch with aliases, tags outside the core schema (the non-specific ! is allowed), keys that repeat by value, or more than one document is refused, and a YAML syntax error in a patch gives its line and column. ApplyMergePatch still reads JSON only. See Patches written in YAML.
  • Document and Node have a String method, so printing one prints its YAML.
  • The YAML11Strings option for Parse. By default an edit judges whether a new string can be plain by the Core schema, so Set("/a", "yes") writes a: yes, a string to this package but a boolean to a program that reads YAML 1.1, such as Kubernetes tooling built on sigs.k8s.io/yaml or code on yaml.v2. With YAML11Strings, new strings written by Set, patches and merge patches, and keys written by Rename, are quoted when a YAML 1.1 reader would read them as something else (yes, on, 0755, 12:30), as Marshal does. Use it when such programs also read the file. See When the style cannot hold the value.
  • TypeError.More and TypeError.All report every value that did not fit (see Changed). ErrUnknownField matches an unknown key under KnownFields, even when it is one of several type errors.
  • CategorySyntax, CategoryEncoding and CategoryDuplicateKey name the values of SyntaxError.Category. A duplicate-key error sets SyntaxError.Key, FirstLine and FirstColumn to the key and the position where it first appeared.

Changed

  • Breaking: decoding goes on past a value that does not fit its target. Unmarshal, Decoder.Decode, Document.Decode and Document.Get skip such a value and read the rest of the document. The target of a skipped value keeps what it held: a nil pointer stays nil, an element of a slice is left zero, and a map entry is not added. One *TypeError then describes the first mismatch, holds the others in More, and lists them all in its message. The read stops at the 100th mismatch, and fewer are reported when their paths together would pass the larger of the input size and 1 MiB, as with long keys reached through aliases. A key repeated before or after an entry that did not fit is still a duplicate-key error. Syntax errors, limit errors and errors from your own hooks still stop the read at once, and are returned without the mismatches collected before them. Code that checks err before it uses the target needs no change. Code that relied on fields after the first bad value staying untouched must not rely on that any more. See the TypeError reference.
  • Breaking: Node.HeadComment and Node.LineComment return trimmed text, as yaml.v3 does: each line starts at its #, without indentation or trailing blanks, and there is no final line break. They also read the comments of the root and the comment after the colon or dash of a block collection, which used to read as "". If you compared or wrote back the raw text, compare with the trimmed form, or use SetHeadComment and SetLineComment to change a comment. See Comments.
  • Breaking: two keys that are different numbers but round to the same float64, such as 1e-400 and 2e-400, or 1e400 and .inf, are a *TypeError that says so, whether you read into any or into a map[float64]T. They used to be a duplicate-key *SyntaxError. Keys that are the same number written differently, such as 1.0 and 1.00, or 1e400 and 10e399, or 1e5 and 1e00005, are still a duplicate key. To read such keys, quote them so that they are strings, or read into a map with string keys (map[string]T), which keeps the text of each key. If you matched these errors with *SyntaxError or CategoryDuplicateKey, match *TypeError too; see the TypeError reference.
  • Any number of range loops over Node.Items or Node.Entries can follow edits at once. A Document used to follow at most three, and a further loop ended at its first edit.
  • Syntax error messages say what to fix, for example expected ',' or ']' after a flow sequence entry (is a closing ']' missing?). A duplicate-key message names the key and where it first appeared. Messages are written for people and may change again; match errors by type, category and kind.

Fixed

  • A plain number passed to an UnmarshalJSON method keeps its digits as written. 12345678901234567890.5 used to arrive rounded to 12345678901234567000. Inside a mapping or sequence passed to UnmarshalJSON, integers keep every digit and floats are still rounded to float64.
  • compat/yamlv3: a duplicate-key error names the key and the line of its first occurrence, as yaml.v3 words it (mapping key "a" already defined at line 1).

0.1.1 - 2026-10-16

Fixed

  • A comment on a --- marker that also carries a tag or anchor, as in --- !!null # note, now stays on the marker line when Set or ApplyMergePatch gives the empty document content. It used to move below the new content.
  • Replacing an empty sequence item that has a comment after its dash (- # note) with a mapping or sequence now keeps the comment on the dash line and writes the new collection on the lines below, lined up with the text of the sibling items. The comment used to move to the end of the collection's last line.
  • Removing an entry that follows a block scalar no longer returns an *EditError when lines of only spaces, indented deeper than the scalar's text, sit below the removed entry. Those lines are removed with the entry, because once it is gone they would become part of the scalar's text.
  • compat/yamlv3: the comments of a document that has a --- marker and no content, such as --- # note, become the document node's FootComment, as yaml.v3 reads them. They used to be lost.
  • compat/yamlv3: a comment after a dash with no value (- # note) becomes the HeadComment of the next item, as yaml.v3 reads it, so writing the Node back keeps it, on its own line above that item. It used to be lost.

0.1.0 - 2026-10-15

The first release. It has no benchmark results; those come in a later update.

Added

  • YAML 1.2 reading with the Core schema by default: Unmarshal, Marshal, NewDecoder, NewEncoder, with encoding/json vocabulary and yaml struct tags (falling back to json tags). Reading UTF-8, UTF-16 and UTF-32.
  • Hooks for types that read and write themselves, tried in this order when reading: Unmarshaler (UnmarshalYAML(oneyaml.Node) error), the older UnmarshalYAML(func(any) error) error form, encoding.TextUnmarshaler for scalars, then json.Unmarshaler. When writing: Marshaler, encoding.TextMarshaler, then json.Marshaler; a number that MarshalJSON returns is written with exactly its digits, bounded by MaxNumberDigits. Types written for encoding/json need no YAML code. An UnmarshalYAML or MarshalYAML method with any other signature, such as yaml.v3's UnmarshalYAML(*yaml.Node) error, is a *TypeError instead of being skipped. See Migrating from yaml.v3.
  • Struct tag options omitempty, omitzero, inline and flow. omitempty leaves out a struct whose fields are all zero, or whose IsZero method returns true, as yaml.v3 does; omitzero leaves out zero values as encoding/json does.
  • Parse and Document: a lossless document with Get, Set, Remove, Decode, Bytes, WriteTo, stream facts and a read-only Node view. Edits change only the value you set, in its own style; comments and formatting elsewhere are kept byte for byte.
  • Node.Pointer, which gives the RFC 6901 pointer of a node so that you can collect places while reading and edit afterwards, and Node.Stale, which reports a node read before an edit. A stale node's Decode and Pointer return an error matching ErrStaleNode, Marshal of a stale node is an error instead of null. A range over Node.Items or Node.Entries may edit the document, as a loop over a map may change the map: after an edit it finds its collection again by pointer and goes on with fresh nodes, and it ends if the collection is gone, changed kind or has no pointer. Edits that add or remove entries may make the loop skip or repeat some; collect pointers first for those. An edit, patch or merge patch that fails is rolled back completely and leaves the nodes read before it valid. ErrNoPointer reports a node that no pointer addresses, such as a mapping key. See Editing.
  • A Node is not a decode target: decoding into one is a *TypeError that points to Document.Get(pointer, &node). Marshal writes a Node as the value it holds.
  • RFC 6902 JSON Patch (ApplyPatch, Apply), with test comparing values by the YAML data model, so 1 and 1.0 differ (details); RFC 6901 pointers; and RFC 7396 merge patch (ApplyMergePatch). All are atomic. See Patches.
  • Limits: depth, alias expansion, number digits and patch work are bounded by default; the Untrusted preset adds bounds on input size, scalar size and documents per stream; NoLimits lifts every bound. Limit options apply in order: Limits changes only the LimitSet fields you set and keeps the rest from the options before it, and NoBound lifts one bound, so Untrusted(), Limits(LimitSet{MaxDepth: 50}) keeps every untrusted bound and tightens depth. See Limits.
  • Typed errors: SyntaxError, TypeError, LimitError, EditError and the sentinel kinds. Methods on a nil *Document return ErrNilDocument or zero values instead of panicking. Messages carry the oneyaml: prefix once; match errors with errors.Is and errors.As, not by their text.
  • compat/yamlv3, a compatibility adapter for gopkg.in/yaml.v3 with the same names and signatures and listed differences. See Migrating from yaml.v3.
  • compat/k8syaml, a compatibility adapter for sigs.k8s.io/yaml with the same names and signatures and listed differences. See Migrating from sigs.k8s.io/yaml.
  • Conformance: the tests that run the oneyaml-kit cases run in CI against a pinned kit revision. See Conformance.
  • Documentation: Editing, Patches, Limits, Conformance, Comparison and the two migration guides.