There are two ways to move code written against YamlDotNet:
- Swap to the adapter.
OneYaml.Compat.YamlDotNetprovides the YamlDotNet serializer builders and representation model under new namespaces. For the API listed below, changing the package and theusinglines keeps your call sites, and YAML 1.1 forms read as YamlDotNet reads them. - Move to the
OneYamlAPI.YamlSerializerandYamlDocumentare YAML 1.2, bounded for untrusted input, and can edit a file without losing its comments. This is the route for new code.
You can do the first now and the second file by file: the two packages work side by side.
Before you swap
A build that passes after the swap is not yet a finished migration. Check these four things, in this order:
| Check | What to look for |
|---|---|
| Compile gaps | API the adapter does not provide fails to build: the scanner, MergingParser, type inspectors, node deserializers, event emitters, object-graph visitors, static contexts and comment events. The full list. |
| Runtime rejections | Deserialize(IParser) with a parser that is not the adapter's own Parser throws NotSupportedException. A serializer built with EnsureRoundtrip() throws NotSupportedException when Serialize is called: the adapter cannot do what it asks, so it says so instead of ignoring it. |
| Semantic changes | Serializer output can differ (more quoting, escapes), more valid YAML is read, the class of an exception can differ, and after you change a node YamlStream.Save writes without comments. See Check after the swap and the list of differences. |
| Safety limits | The depth limit and the alias budget always apply, and WithMaximumRecursion cannot raise the depth above the library limit. For input you do not control, pass YamlLimits.Untrusted to WithLimits (Input you do not control). WithLimits is an addition: YamlDotNet has no such setting. |
Swap to the adapter
dotnet remove package YamlDotNet dotnet add package OneYaml.Compat.YamlDotNet
| Before | After |
|---|---|
using YamlDotNet.Serialization; | using OneYaml.Compat.YamlDotNet.Serialization; |
using YamlDotNet.Serialization.NamingConventions; | using OneYaml.Compat.YamlDotNet.Serialization.NamingConventions; |
using YamlDotNet.RepresentationModel; | using OneYaml.Compat.YamlDotNet.RepresentationModel; |
using YamlDotNet.Core; | using OneYaml.Compat.YamlDotNet.Core; |
Before, with YamlDotNet 18.1.0:
using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; IDeserializer deserializer = new DeserializerBuilder() .WithNamingConvention(CamelCaseNamingConvention.Instance) .IgnoreUnmatchedProperties() .Build(); var service = deserializer.Deserialize<Service>("name: web\nreplicas: 2\nlegacy: true\n"); ISerializer serializer = new SerializerBuilder() .WithNamingConvention(CamelCaseNamingConvention.Instance) .Build(); Console.Write(serializer.Serialize(service)); // name: web // replicas: 2 class Service { public string Name { get; set; } = ""; public int Replicas { get; set; } }
After, with the adapter; only the using lines changed, and the output is the same:
using OneYaml.Compat.YamlDotNet.Serialization; using OneYaml.Compat.YamlDotNet.Serialization.NamingConventions; IDeserializer deserializer = new DeserializerBuilder() .WithNamingConvention(CamelCaseNamingConvention.Instance) .IgnoreUnmatchedProperties() .Build(); var service = deserializer.Deserialize<Service>("name: web\nreplicas: 2\nlegacy: true\n"); ISerializer serializer = new SerializerBuilder() .WithNamingConvention(CamelCaseNamingConvention.Instance) .Build(); Console.Write(serializer.Serialize(service)); // name: web // replicas: 2 class Service { public string Name { get; set; } = ""; public int Replicas { get; set; } }
What the adapter covers
Serialization:DeserializerBuilderandSerializerBuilderwithWithNamingConvention,WithEnumNamingConvention,WithTypeConverter,WithTagMapping,WithAttributeOverride,IgnoreUnmatchedProperties,WithCaseInsensitivePropertyMatching,WithDuplicateKeyChecking,WithAttemptingUnquotedStringTypeDeserialization,WithMaximumRecursion,WithTypeMapping,WithObjectFactory,ConfigureDefaultValuesHandling,WithQuotingNecessaryStrings,WithDefaultScalarStyle,DisableAliases,JsonCompatible,WithNewLineandWithIndentedSequences;IDeserializer,ISerializer, the six naming conventions,[YamlMember],[YamlIgnore],[YamlConverter],IYamlTypeConverterandIYamlConvertible.RepresentationModel:YamlStream,YamlDocument,YamlMappingNode,YamlSequenceNode,YamlScalarNode, visitors, node positions.Core:Parser,Emitter, the event classes,Markand the exception types.
The list of differences names what is not provided, such as the scanner, MergingParser, type inspectors, node deserializers and static contexts. Code that uses them does not compile, so the gap shows at build time.
The adapter's representation model reads a file and writes it back as it was when nothing changed:
using OneYaml.Compat.YamlDotNet.RepresentationModel; var stream = new YamlStream(); stream.Load(new StringReader("# service settings\nname: web\nreplicas: 2 # set by CI\n")); var root = (YamlMappingNode)stream.Documents[0].RootNode; Console.WriteLine(root["replicas"]); // 2 var text = new StringWriter(); stream.Save(text); // nothing was changed, so the text comes back as it was read Console.Write(text); // # service settings // name: web // replicas: 2 # set by CI
After you change a node, Save writes the model again without comments, as YamlDotNet does. To change a file and keep its comments, use YamlDocument (below).
Check after the swap
The adapter reproduces YamlDotNet's documented behaviour, not its defects. Read the full list of differences, each with a repro, and check these first:
- Output that reads back. Strings that a reader would take for something else are quoted (
'true','yes','123'), characters that YamlDotNet writes in a way that changes on reading are escaped, and long keys are written so that they read back. Snapshot tests of serializer output may need updating. - Comments survive an unchanged round trip.
YamlStream.Savereturns the loaded text when nothing changed; YamlDotNet writes it again without comments and with...after each document. - More valid YAML is read. Some inputs that YamlDotNet refuses or throws
ArgumentExceptionon are valid YAML 1.2 and are read. - Limits stay on. The deserializer enforces the alias budget and the depth limit. An alias into
objector a collection gives back the same instance, and every use is charged to the alias budget as if it were expanded, so a billion-laughs input fails with aYamlException. For input you do not control, callWithLimits(YamlLimits.Untrusted)on the builder, or passYamlLimits.UntrustedtoYamlStream.Loadornew Parser(reader, limits)(Input you do not control). - Exception classes.
SyntaxErrorException,SemanticErrorException,MaximumRecursionLevelReachedExceptionandAnchorNotFoundExceptionare raised, but which class a given error gets can differ from YamlDotNet. CatchYamlExceptionif you can. - Line breaks. The serializer writes the platform line break, as YamlDotNet does.
Input you do not control
The adapter adds three members that YamlDotNet does not have, so that reading can keep to the limits of the OneYaml library: DeserializerBuilder.WithLimits(YamlLimits), YamlStream.Load(TextReader, YamlLimits) and new Parser(TextReader, YamlLimits). Without them the adapter reads with YamlLimits.Default. For uploads, request bodies and files from other systems, pass YamlLimits.Untrusted, which also bounds the input size, the size of a scalar and the number of documents. YamlLimits is in the OneYaml namespace; import it with using YamlLimits = OneYaml.YamlLimits;, not using OneYaml;, because that namespace has its own YamlException, YamlDocument, YamlNode and YamlIgnoreAttribute, and importing it beside the adapter's namespaces makes those names ambiguous (error CS0104).
using OneYaml.Compat.YamlDotNet.Core; using OneYaml.Compat.YamlDotNet.Serialization; using YamlLimits = OneYaml.YamlLimits; string upload = new string('[', 129) + new string(']', 129); // nested 129 deep // WithLimits is not in YamlDotNet. It takes the limits of the OneYaml library; Untrusted is its preset for outside input. IDeserializer deserializer = new DeserializerBuilder() .WithLimits(YamlLimits.Untrusted) .Build(); try { deserializer.Deserialize<object>(upload); } catch (YamlException e) { Console.WriteLine(e.Message); // The nesting depth is larger than the limit MaxDepth (128). (line 1, column 129) } // The default limits allow the same text. Console.WriteLine(new DeserializerBuilder().Build().Deserialize<object>(upload) is not null); // True
Code that uses these members no longer compiles against YamlDotNet itself.
Move to the OneYaml API
dotnet add package OneYamlIf the project still references OneYaml.Compat.YamlDotNet, keep OneYaml at the same version: the adapter depends on that exact version.
using OneYaml; // camelCase keys and skipping unknown keys are the defaults; no builder is needed. var service = YamlSerializer.Deserialize<Service>("name: web\nreplicas: 2\nlegacy: true\n")!; Console.Write(YamlSerializer.Serialize(service)); // name: web // replicas: 2 class Service { public string Name { get; set; } = ""; public int Replicas { get; set; } }
| YamlDotNet | OneYaml |
|---|---|
new DeserializerBuilder().Build().Deserialize<T>(yaml) | YamlSerializer.Deserialize<T>(yaml) |
new SerializerBuilder().Build().Serialize(value) | YamlSerializer.Serialize(value) |
WithNamingConvention(CamelCaseNamingConvention.Instance) | the default (PropertyNamingPolicy = JsonNamingPolicy.CamelCase) |
UnderscoredNamingConvention, HyphenatedNamingConvention | JsonNamingPolicy.SnakeCaseLower, JsonNamingPolicy.KebabCaseLower |
NullNamingConvention (YamlDotNet's default) | PropertyNamingPolicy = null |
IgnoreUnmatchedProperties() | the default; UnmappedMemberHandling = Disallow is YamlDotNet's default behaviour |
WithCaseInsensitivePropertyMatching() | PropertyNameCaseInsensitive = true |
WithDuplicateKeyChecking() | always on: a repeated key is an error |
WithMaximumRecursion(n) | Limits = YamlLimits.Default with { MaxDepth = n } |
ConfigureDefaultValuesHandling(OmitNull) | DefaultIgnoreCondition = YamlIgnoreCondition.WhenWritingNull |
WithTypeConverter(IYamlTypeConverter) | Converters = { new MyConverter() }, a YamlConverter<T> (Converters) |
WithTagMapping("!x", typeof(X)) | [YamlPolymorphic(DiscriminatorKind = YamlDiscriminatorKind.Tag)] and [YamlDerivedType(typeof(X), "x")] on a base type (Polymorphism) |
[YamlMember(Alias = "x")], [YamlMember(Order = n)] | [YamlPropertyName("x")], [YamlPropertyOrder(n)] |
[YamlIgnore] | [YamlIgnore] (in the OneYaml namespace) |
MergingParser | MergeKeys = true |
YamlStream, YamlMappingNode, YamlScalarNode | YamlDocument, YamlNode (Editing) |
YamlException.Start | YamlException.Line, Column, Offset, plus Category and Path |
| static builders with a separate generator package | [YamlSerializable] on a YamlSerializerContext; the generator is in the package (Source generation) |
Behaviour to expect when you move:
- YAML 1.2, not 1.1.
yes,onandyare strings, so readingenabled: yesinto aboolis an error; writetrue.017is 17, not octal 15;0o17is 15. - Untyped values have types.
Deserialize<object>giveslong,double,boolandnullfor plain scalars, where YamlDotNet gives strings (Untyped values). - Repeated keys are an error, where YamlDotNet keeps the last one by default.
nullmembers are written asnullunless you setDefaultIgnoreCondition.
Editing a file
YamlDotNet's representation model writes a file again from the model, so comments and blank lines are lost when you save a change. YamlDocument changes only the value you set:
// Instead of YamlStream: load, change one value, save, and keep the comments. using var doc = YamlDocument.Parse("# service settings\nname: web\nreplicas: 2 # set by CI\n"); doc.Root["replicas"] = 3; Console.Write(doc); // # service settings // name: web // replicas: 3 # set by CI
See Editing for pointers, styles and aliases, and Patches to apply a JSON Patch.