---
title: "Changelog"
url: https://recursiveai.org/oneyaml/csharp/docs/changelog/
description: "All notable changes are listed here, newest first. The format follows Keep a Changelog, and versions follow Semantic Versioning. A change that can break…"
version: "1.2.1"
source: https://github.com/recursive-ai-oss/oneyaml-csharp/blob/7818048c4be434333767c47d66176fc626d1da8e/CHANGELOG.md
---

# Changelog

All notable changes are listed here, newest first. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and versions follow [Semantic Versioning](https://semver.org/). 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](https://recursiveai.org/oneyaml/csharp/docs/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](https://github.com/recursive-ai-oss/oneyaml-csharp/blob/v1.0.0/README.md#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](https://github.com/recursive-ai-oss/oneyaml-csharp/blob/v1.0.0/docs/serializer.md).
  - `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](https://github.com/recursive-ai-oss/oneyaml-csharp/blob/v1.0.0/docs/editing.md).
  - `ApplyPatch` (JSON Patch, RFC 6902) and `ApplyMergePatch` (JSON Merge Patch, RFC 7396) change a `YamlDocument` atomically and keep its layout. See [Patches](https://github.com/recursive-ai-oss/oneyaml-csharp/blob/v1.0.0/docs/patches.md).
  - `YamlConverter<T>` with `Utf8YamlReader` for custom types, and `Utf8YamlEmitter` to write YAML token by token. See [Reader and emitter](https://github.com/recursive-ai-oss/oneyaml-csharp/blob/v1.0.0/docs/reader-emitter.md).
  - `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](https://github.com/recursive-ai-oss/oneyaml-csharp/blob/v1.0.0/docs/migrate-yamldotnet.md) and the [differences from YamlDotNet](https://github.com/recursive-ai-oss/oneyaml-csharp/blob/v1.0.0/docs/compat/yamldotnet.md).
- **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](https://github.com/recursive-ai-oss/oneyaml-csharp/blob/v1.0.0/docs/migrate-sharpyaml.md) and the [differences from SharpYaml](https://github.com/recursive-ai-oss/oneyaml-csharp/blob/v1.0.0/docs/compat/sharpyaml.md).
- **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](https://github.com/recursive-ai-oss/oneyaml-csharp/blob/v1.0.0/docs/limits.md).

### 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](https://github.com/recursive-ai-oss/oneyaml-csharp/blob/v1.0.0/docs/patches.md#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](https://github.com/recursive-ai-oss/oneyaml-csharp/blob/v1.0.0/docs/editing.md#what-an-edit-keeps-and-what-it-does-not).
- **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.

[Unreleased]: https://github.com/recursive-ai-oss/oneyaml-csharp/compare/v1.2.1...HEAD
[1.2.1]: https://github.com/recursive-ai-oss/oneyaml-csharp/compare/v1.2.0...v1.2.1
[1.2.0]: https://github.com/recursive-ai-oss/oneyaml-csharp/compare/v1.1.1...v1.2.0
[1.1.1]: https://github.com/recursive-ai-oss/oneyaml-csharp/compare/v1.1.0...v1.1.1
[1.1.0]: https://github.com/recursive-ai-oss/oneyaml-csharp/compare/v1.0.0...v1.1.0
[1.0.0]: https://github.com/recursive-ai-oss/oneyaml-csharp/releases/tag/v1.0.0
