Recursive AI

Changelog

All oneyaml C# docs

All notable changes are listed here, newest first. The format follows Keep a Changelog, and versions follow Semantic Versioning. A change that can break your code is marked Breaking and says what to write instead. What a release promises, and the supported .NET versions, are under Stability.

Unreleased

1.2.1 - 2026-10-18

Fixed

  • A stream or reader too large for one buffer is a Limit error, whatever MaxInputBytes says. With no input limit, or a limit above 2 GiB, a stream or reader larger than an array can hold, or one that never ends, could fail with another exception:

    • YamlSerializer.Deserialize, DeserializeAll and the compat readers over a TextReader threw ArgumentOutOfRangeException when no limit was set;
    • OneYaml.Compat.SharpYaml over a Stream threw IOException ("Stream was too long") or ran out of memory.

    They now stop with the usual limit error: a YamlException with category Limit, or in the compat packages the exception type that package already uses for a limit. With no limit set, a reader other than a StringReader (whose text is already in memory) is now read in chunks, as it already was under a limit, instead of in one call.

  • OneYaml.Compat.YamlDotNet error marks after non-ASCII text count characters, not bytes. Start.Index of a syntax error in text read from a string or a reader was the UTF-8 byte offset, so it was too large once a multi-byte character came before the error. It now counts UTF-16 characters, as YamlDotNet does, and a character outside the Basic Multilingual Plane counts as two columns, as in YamlDotNet. A leading byte order mark is still not counted.

  • OneYaml.Compat.SharpYaml holds the whole text to MaxInputBytes, not only its first document. A string, span, reader or stream with several documents was cut at the second document before the limit was checked, so Deserialize<object>("a: 1\n---\nb: 2\n", ...) with MaxInputBytes = 13 returned the first document instead of the limit error. The size of a string, span or reader is the UTF-8 size of its text without a leading byte order mark, as the core counts a string. A stream is held to its encoded size without the mark, and is read only up to the limit plus one buffer.

  • OneYaml.Compat.SharpYaml error marks in a string that starts with a byte order mark are no longer shifted. The marks (line, column and index) counted the mark as a character, and counted its bytes in the wrong place, so the same text gave different positions as a string and as a stream or reader. A string that began with a mark, a directive and more than one document could also read a wrong value (a: 1 came back as a: null). The mark is now never a column, whatever the input.

  • OneYaml.Compat.YamlDotNet marks of nodes and errors after a leading byte order mark are no longer shifted. When the text came from a string, or from a reader that keeps the mark, Start and End of every node after the first, and of an error, were off by the mark. They now equal the marks of the same text without it.

1.2.0 - 2026-10-18

Added

  • OneYaml.Compat.YamlDotNet tells an alias budget from a depth limit. AliasExpansionLimitExceededException derives from MaximumRecursionLevelReachedException and is thrown where reading or writing expanded aliases passes the alias budget. Depth and size limits still throw the base type, and a catch (MaximumRecursionLevelReachedException) still catches both. MaximumRecursionLevelReachedException is no longer sealed so that this is possible.

Changed

  • A run of replacements on a large document no longer rewrites the offsets of everything behind each edit. ApplyPatch and ApplyMergePatch edit one state in place, and each edit that kept the number of nodes moved the offsets of every later node: about 0.1 ms per edit at 100,000 keys, whatever the edit. On documents of 4,096 nodes or more, from the second edit of a patch on, that move is now noted per chunk of 64 nodes and carried out when a chunk is next read, so a run of edits costs what the edits touch. Measured, not a bound: parse, apply and write of 10,000 replacements on a mapping of 100,000 keys takes 0.15 s (was 1.05 s) and of 100,000 replacements 1.6 s (was 10.6 s); a 100-operation patch on an 8 MB document of records takes about 95 ms (was about 145 ms), mostly parsing. Output bytes, errors, positions and the stack a deep document needs are unchanged, and so is the memory a parsed document keeps. What still grows with the document: an edit that adds or removes nodes (add, remove, a collection value) moves the node array behind it, and an edit that changes the length of the text moves the text behind it; the comment and property tables are walked in full.
  • A loop of single edits no longer copies the whole document for each edit. (Breaking in one rare case: a document whose restore after a failed edit ran out of memory now refuses further use with InvalidOperationException; parse the text again to continue.) Set, Remove, the YamlNode indexers and the other single edits used to build a full copy of the document's state and swap it in, so each edit cost the size of the document. They now edit the document's own state in place. If an edit fails, the document is left as it was: most failures are found before anything changes, and an edit that fails after it has begun changing the text parses the old text again, so only that error path pays for it. The one exception is running out of memory while the old text is parsed again: the error you get is the edit's own, and the document then refuses further use with an InvalidOperationException (Dispose still works). Any other failure during that step would be a defect in the library and is not hidden. Output bytes, errors, positions and live views are unchanged. Measured, not a bound (parse and write included): 1,000 edits on a mapping of 100,000 keys take 0.09 s through the indexer (was 0.23 s) and 0.18 s through Set (was 0.24 s); 10,000 take 0.8 s (was 2.3 s); 1,000 edits on the 8 MB records file take 0.9 s (was 4.7 s) and 10,000 take 8.2 s (was 46 s); setting one field on every item of a 1 MB records file takes 0.35 s (was 0.7 s). One Set on a parsed document is 1.0 to 2.6 times as fast on the benchmark files and allocates about 120 bytes less; parsing, reading and writing back are as fast as before and allocate the same. What still grows with the document: an edit moves the offsets of the nodes behind it, an edit that adds or removes nodes moves the node array, an edit that changes the length of the text moves the text behind it, and the comment and property tables are walked in full. ApplyPatch and ApplyMergePatch hold some of that move back across a run of edits and still do, so a patch of many operations stays faster than the same edits made one call at a time.
  • A key index lost to a race is built again. When two readers raced to index two large mappings, one could leave its table in the slot of the other, and the other mapping stayed unindexed until an edit. The slot now counts as free.

Fixed

  • A text reader is no longer read to its end before the input limit applies. YamlSerializer.Deserialize and DeserializeAll over a TextReader read the whole reader and only then compared its size with MaxInputBytes, so a very large or endless reader filled memory first, even under YamlLimits.Untrusted. They now read in chunks, count the UTF-8 size as they go and stop with the usual Limit error as soon as the text is over the limit, after reading at most one chunk (4,096 characters) beyond it. A text exactly at the limit still reads, and a leading byte order mark is not counted, as for a string. When no input size limit is set (MaxInputBytes left at its default, as in YamlLimits.Default and YamlLimits.None), the reader is read in one call as before.
  • Compat readers over a TextReader apply the input limit as the core does. YamlStream.Load(TextReader, limits) and the other YamlDotNet readers stopped reading at MaxInputBytes characters and parsed what they had, so a text with a byte order mark that was one character over the limit lost its tail without an error; it is now a Limit error. The SharpYaml Deserialize overloads over a TextReader refused a text of exactly the limit when it began with a byte order mark, which a string of the same text is allowed; they now accept it. Both use the core's chunked reader.
  • OneYaml.Compat.YamlDotNet writes Int128, UInt128 and Half as numbers and reads them back. They were written as {} (as YamlDotNet does), which lost the value. A Half too large for the type is an error rather than infinity.
  • Breaking: OneYaml.Compat.YamlDotNet reports a repeated key read from a parser as a YamlException. YamlStream.Load(IParser) threw ArgumentException; it now throws the library's YamlException, positioned at the repeated key, as YamlDotNet itself does. Code that caught ArgumentException around Load(IParser) should catch YamlException instead.
  • A key in extension data now beats the same key merged into the same merge source. With MergeKeys on, a mapping read through a merge key (<<: *base) whose own << brought in x and which also gave x itself kept the merged value when the type has an extension data member; the mapping's own x now wins, wherever its << stands, as it already did for members and dictionaries. The first source still wins among several, and the keys of the mapping that is read itself still beat every source.
  • OneYaml.Compat.YamlDotNet no longer reads the error text to tell an alias budget from a depth limit. The code that refuses the expansion marks the core error, so rewording a message cannot change which exception is thrown.

1.1.1 - 2026-10-17

Changed

  • Many operations on one large mapping no longer slow down with its size. Looking up a key, working out the indentation to write and checking for duplicate keys each walked the whole mapping on every operation. On a mapping of 100,000 keys, 10,000 patch operations took 19.1 s to remove, 11.9 s to replace and 3.4 s to test; they take about 1.5 s, 1.1 s and 26 ms now (ApplyPatch, ApplyMergePatch, Set, Remove and the indexers share the change). A mapping of 256 or more entries gets a key index once searching it in order has cost a few times its size, so a small mapping, or one read once or twice, behaves and costs as before; the read that builds the index costs a few searches more, and later reads are quicker; the index stays correct across edits, and a mapping with two keys of the same text is searched in order as before. Output bytes, errors and positions are unchanged. A document that is only parsed keeps its size; once an index is built the document holds it until it is disposed, about 13 to 32 bytes per entry (21 at 100,000 keys). What still grows with the document is moving the entries after an edit, about 80 microseconds per edit at 100,000 keys.
  • Faster number reads. Plain integers of up to 18 digits and plain decimal numbers with a short mantissa and exponent are read in one pass, in typed and generic loads and wherever a float key is compared. Loading number-heavy input is 5 to 14 percent faster; every value, error, category and position is unchanged, and hexadecimal, octal, .inf, .nan, long and out-of-range numbers take the same path as before.

Fixed

  • Error positions in UTF-16 and UTF-32 input. A malformed UTF-16 or UTF-32 input, a lone surrogate for example, was reported one column too far, with or without a byte order mark; the column now names the bad character. The Offset of such an error still counts bytes of the original input, the byte order mark included. Other errors in these encodings already had the column and offset of the same text read as UTF-8, and a test now holds that.

1.1.0 - 2026-10-17

Changed

  • Get<object> and GetValue<object> read collections. Asking a node for object, through YamlDocument.Get<object>, TryGet, YamlNode.GetValue<object> or TryGetValue, now gives what YamlSerializer.Deserialize<object> gives: a Dictionary<string, object?> for a mapping and a List<object?> for a sequence. It used to fail with "The node is not a scalar".
  • A reader that was copied and left behind says so. A Utf8YamlReader shares its scanner with its copies. When a copy read past the end of its value, the original used to fail with a syntax error that blamed the input. It now throws InvalidOperationException that names the misuse; the same holds for a copy used after the original read on. Looking ahead on a copy inside the current value still works. The failure is reported when the reader hits the damaged position, so a copy that damaged a reader without causing an error stays undetected: do not copy the reader.
  • Faster write-back of an unedited document. YamlDocument.ToUtf8Bytes no longer clears the output array before copying into it.
  • Error messages. A refused alias expansion reads the same from a document and from the serializer, and names a position. A key discriminator that is missing points at the mapping, not at its first key. The SharpYaml adapter's messages state the position once.

Fixed

  • An explicit key beats a merged one inside a merge source. With MergeKeys on, a mapping used as the source of a << key could hold its own << and then an explicit key, as in {<<: {a: 1}, a: 2}. Reached through the outer merge, a typed load into a class, record or dictionary gave a the merged value 1. It now gives 2, as when that mapping is read directly and as generic load does. The first source still wins among several, and a repeated key inside one source is still an error.
  • A repeated key inside a merge source is an error. With MergeKeys on, a mapping used as the source of a << key could repeat a key that binds to a member or a dictionary entry, and the second value was dropped without a word. It now fails with DuplicateKey at the repeated key, as a repeated key does anywhere else. The same key in two different sources is still fine; the first source wins.
  • The YamlDotNet adapter's Serializer bounds a graph that shares its parts. With DisableAliases() or JsonCompatible(), a graph in which objects are reachable many times over was written out in full without limit. The nodes written for repeated objects are now held to the alias budget of the limits and the writer throws MaximumRecursionLevelReachedException beyond it. SerializerBuilder.WithLimits sets the limits; the default is YamlLimits.Default.
  • The documentation of MaxDepth in the SharpYaml adapter now matches what it does: the property reads back the value set, and the lower of it and Limits.MaxDepth applies.

1.0.0 - 2026-10-16

The first release: a YAML 1.2 library for .NET 8 and .NET 10 that reads and writes .NET types, and edits a file while keeping its comments and layout. From this release the public API follows the promises under Stability.

Added

  • The OneYaml package.
    • YamlSerializer reads YAML into .NET types and writes .NET values as YAML, in the shape of System.Text.Json: YamlSerializerOptions, naming policies (camel case by default), custom converters, polymorphism by key or by tag, extension data, and the System.Text.Json attributes where no YAML attribute is present. A source generator in the package writes metadata for trimmed and native AOT applications. See Serializer.
    • YamlDocument edits a file through YamlNode indexers or RFC 6901 pointer methods and keeps every byte you do not change. A new value is written in the style of the old one, or the nearest style that can hold it. See Editing.
    • ApplyPatch (JSON Patch, RFC 6902) and ApplyMergePatch (JSON Merge Patch, RFC 7396) change a YamlDocument atomically and keep its layout. See Patches.
    • YamlConverter<T> with Utf8YamlReader for custom types, and Utf8YamlEmitter to write YAML token by token. See Reader and emitter.
    • YamlException reports bad or oversized input with a stable Category, the line, column and byte offset, and the path of a value that failed to bind.
  • The OneYaml.Compat.YamlDotNet package, for code written against YamlDotNet 18: the serializer builders and the representation model under OneYaml.Compat.YamlDotNet namespaces. See Migrating from YamlDotNet and the differences from YamlDotNet.
  • The OneYaml.Compat.SharpYaml package, for code written against the SharpYaml 3 serializer: YamlSerializer, YamlSerializerOptions and the attributes under the OneYaml.Compat.SharpYaml namespace. See Migrating from SharpYaml and the differences from SharpYaml.
  • Limits. YamlLimits has the presets Default, Untrusted and None. Default applies whenever you pass no limits and bounds nesting depth, alias expansion and the digits of an integer. Untrusted also bounds input size, scalar size and documents per stream; use it for input you do not control. The adapters read with Default unless you give them limits through members their targets do not have: WithLimits, and limits overloads of YamlStream.Load and the Parser constructor, for YamlDotNet code; YamlSerializerOptions.Limits for SharpYaml code. See Limits.

Behaviour to know before you upgrade from another library

  • YAML 1.2 Core schema by default. Plain yes is a string, 0755 is the integer 755, a key written twice in a mapping is an error, and << merges only with the MergeKeys option. The compatibility adapters read the YAML 1.1 forms their targets document.
  • test in a patch compares YAML values, not JSON values. An integer and a float are different values, so 1 does not equal 1.0, and a plain key 80 is an integer that does not match a member "80". See Differences from RFC 6902.
  • What an edit keeps. Removing an entry also removes the comment lines just above it, replacing a whole mapping or sequence discards the comments inside it, and writes through an alias are refused. There is no API to read or write comments. See What an edit keeps.
  • The adapters refuse what they cannot do. Options an adapter cannot follow, such as YamlDotNet's EnsureRoundtrip and SharpYaml's layout options, throw NotSupportedException instead of being ignored. Each is listed on the adapter's differences page.