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
Limiterror, whateverMaxInputBytessays. 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,DeserializeAlland the compat readers over aTextReaderthrewArgumentOutOfRangeExceptionwhen no limit was set;OneYaml.Compat.SharpYamlover aStreamthrewIOException("Stream was too long") or ran out of memory.
They now stop with the usual limit error: a
YamlExceptionwith categoryLimit, or in the compat packages the exception type that package already uses for a limit. With no limit set, a reader other than aStringReader(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.YamlDotNeterror marks after non-ASCII text count characters, not bytes.Start.Indexof 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.SharpYamlholds the whole text toMaxInputBytes, 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, soDeserialize<object>("a: 1\n---\nb: 2\n", ...)withMaxInputBytes = 13returned 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.SharpYamlerror 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: 1came back asa: null). The mark is now never a column, whatever the input.OneYaml.Compat.YamlDotNetmarks 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,StartandEndof 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.YamlDotNettells an alias budget from a depth limit.AliasExpansionLimitExceededExceptionderives fromMaximumRecursionLevelReachedExceptionand is thrown where reading or writing expanded aliases passes the alias budget. Depth and size limits still throw the base type, and acatch (MaximumRecursionLevelReachedException)still catches both.MaximumRecursionLevelReachedExceptionis 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.
ApplyPatchandApplyMergePatchedit 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, theYamlNodeindexers 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 anInvalidOperationException(Disposestill 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 throughSet(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). OneSeton 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.ApplyPatchandApplyMergePatchhold 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.DeserializeandDeserializeAllover aTextReaderread the whole reader and only then compared its size withMaxInputBytes, so a very large or endless reader filled memory first, even underYamlLimits.Untrusted. They now read in chunks, count the UTF-8 size as they go and stop with the usualLimiterror 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 (MaxInputBytesleft at its default, as inYamlLimits.DefaultandYamlLimits.None), the reader is read in one call as before. - Compat readers over a
TextReaderapply the input limit as the core does.YamlStream.Load(TextReader, limits)and the other YamlDotNet readers stopped reading atMaxInputBytescharacters 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 aLimiterror. The SharpYamlDeserializeoverloads over aTextReaderrefused 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.YamlDotNetwritesInt128,UInt128andHalfas numbers and reads them back. They were written as{}(as YamlDotNet does), which lost the value. AHalftoo large for the type is an error rather than infinity.- Breaking:
OneYaml.Compat.YamlDotNetreports a repeated key read from a parser as aYamlException.YamlStream.Load(IParser)threwArgumentException; it now throws the library'sYamlException, positioned at the repeated key, as YamlDotNet itself does. Code that caughtArgumentExceptionaroundLoad(IParser)should catchYamlExceptioninstead. - A key in extension data now beats the same key merged into the same merge source. With
MergeKeyson, a mapping read through a merge key (<<: *base) whose own<<brought inxand which also gavexitself kept the merged value when the type has an extension data member; the mapping's ownxnow 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.YamlDotNetno 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,Removeand 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
Offsetof 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>andGetValue<object>read collections. Asking a node forobject, throughYamlDocument.Get<object>,TryGet,YamlNode.GetValue<object>orTryGetValue, now gives whatYamlSerializer.Deserialize<object>gives: aDictionary<string, object?>for a mapping and aList<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
Utf8YamlReadershares 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 throwsInvalidOperationExceptionthat 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.ToUtf8Bytesno 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
MergeKeyson, 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 gaveathe 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
MergeKeyson, 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 withDuplicateKeyat 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
Serializerbounds a graph that shares its parts. WithDisableAliases()orJsonCompatible(), 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 throwsMaximumRecursionLevelReachedExceptionbeyond it.SerializerBuilder.WithLimitssets the limits; the default isYamlLimits.Default. - The documentation of
MaxDepthin the SharpYaml adapter now matches what it does: the property reads back the value set, and the lower of it andLimits.MaxDepthapplies.
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
OneYamlpackage.YamlSerializerreads 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.YamlDocumentedits a file throughYamlNodeindexers 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) andApplyMergePatch(JSON Merge Patch, RFC 7396) change aYamlDocumentatomically and keep its layout. See Patches.YamlConverter<T>withUtf8YamlReaderfor custom types, andUtf8YamlEmitterto write YAML token by token. See Reader and emitter.YamlExceptionreports bad or oversized input with a stableCategory, the line, column and byte offset, and the path of a value that failed to bind.
- The
OneYaml.Compat.YamlDotNetpackage, for code written against YamlDotNet 18: the serializer builders and the representation model underOneYaml.Compat.YamlDotNetnamespaces. See Migrating from YamlDotNet and the differences from YamlDotNet. - The
OneYaml.Compat.SharpYamlpackage, for code written against the SharpYaml 3 serializer:YamlSerializer,YamlSerializerOptionsand the attributes under theOneYaml.Compat.SharpYamlnamespace. See Migrating from SharpYaml and the differences from SharpYaml. - Limits.
YamlLimitshas the presetsDefault,UntrustedandNone.Defaultapplies whenever you pass no limits and bounds nesting depth, alias expansion and the digits of an integer.Untrustedalso bounds input size, scalar size and documents per stream; use it for input you do not control. The adapters read withDefaultunless you give them limits through members their targets do not have:WithLimits, and limits overloads ofYamlStream.Loadand theParserconstructor, for YamlDotNet code;YamlSerializerOptions.Limitsfor SharpYaml code. See Limits.
Behaviour to know before you upgrade from another library
- YAML 1.2 Core schema by default. Plain
yesis a string,0755is the integer 755, a key written twice in a mapping is an error, and<<merges only with theMergeKeysoption. The compatibility adapters read the YAML 1.1 forms their targets document. testin a patch compares YAML values, not JSON values. An integer and a float are different values, so1does not equal1.0, and a plain key80is 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
EnsureRoundtripand SharpYaml's layout options, throwNotSupportedExceptioninstead of being ignored. Each is listed on the adapter's differences page.