Recursive AI

YAML in a C# native AOT app

YAML in a C# native AOT app: OneYaml’s built-in source generator, the trim warnings you get without it, and how YamlDotNet and SharpYaml do it.

To use YAML in a C# native AOT app, bind it through source-generated metadata instead of reflection. Native AOT trims away the members that a reflection-based serializer reads at run time, so a plain Deserialize<T>(yaml) call warns at publish time and can fail once published. With OneYaml you declare a YamlSerializerContext, list your root types with [YamlSerializable], and pass the generated metadata to YamlSerializer. The generator ships inside the OneYaml package, the pattern is the one System.Text.Json uses, and YamlDocument edits that keep comments publish without warnings too. The code on this page uses OneYaml 1.2.1 on .NET 10 and was published with native AOT for linux-x64.

1. Turn on native AOT and add the package

shell
dotnet new console -f net10.0
dotnet add package OneYaml --version 1.2.1
MyApp.csprojexcerpt
<PropertyGroup>
  <TargetFramework>net10.0</TargetFramework>
  <PublishAot>true</PublishAot>
</PropertyGroup>

OneYaml targets .NET 8 and .NET 10, the two runtimes it supports (stability). There is no second package to add for the generator.

2. Declare a context and pass its metadata

Program.csOneYaml 1.2.1 · .NET 10
using OneYaml;
 
string yaml = """
    name: billing-api
    replicas: 3
    endpoint: https://billing.internal.example
    labels:
      tier: backend
      team: payments
    """;
 
ServiceConfig config = YamlSerializer.Deserialize(yaml, AppYamlContext.Default.ServiceConfig)!;
Console.WriteLine($"{config.Name}: {config.Replicas} replicas at {config.Endpoint.Host}");
Console.WriteLine($"labels: {string.Join(", ", config.Labels.Select(l => $"{l.Key}={l.Value}"))}");
 
Console.Write(YamlSerializer.Serialize(config with { Replicas = 5 }, AppYamlContext.Default.ServiceConfig));
 
record ServiceConfig(string Name, int Replicas, Uri Endpoint, Dictionary<string, string> Labels);
 
[YamlSerializable(typeof(ServiceConfig))]
partial class AppYamlContext : YamlSerializerContext;
Output
billing-api: 3 replicas at billing.internal.example
labels: tier=backend, team=payments
name: billing-api
replicas: 5
endpoint: https://billing.internal.example
labels:
  tier: backend
  team: payments

The generator writes a YamlTypeInfo<T> property on the context for each [YamlSerializable] type and for every type it reaches, so Dictionary<string, string> and Uri need no attribute of their own. Records with constructor parameters bind as they do with reflection, and so do the System.Text.Json attributes, converters and polymorphism (serializer). For options other than the defaults, such as a naming policy, create the context with them: new AppYamlContext(new YamlSerializerOptions { ... }).

3. Publish

shell
dotnet publish -c Release -r linux-x64

The publish finished with no trim or AOT warnings, and the output is one native executable that prints the same lines as above. On linux-x64 with .NET SDK 10.0.400 it was 2,377,208 bytes, plus a separate .dbg symbols file. Your size will differ with your code and target.

What happens if you call the reflection overload

The overloads that take YamlSerializerOptions use reflection, and OneYaml marks them [RequiresUnreferencedCode] and [RequiresDynamicCode], so the compiler tells you before anything breaks. Here is the same app written the reflection way:

Program.csOneYaml 1.2.1 · .NET 10
using OneYaml;
 
var config = YamlSerializer.Deserialize<ServiceConfig>("name: billing-api\nreplicas: 3\n");
Console.WriteLine($"{config!.Name}: {config.Replicas} replicas");
 
record ServiceConfig(string Name, int Replicas);

It runs under dotnet run, but dotnet publish reports both warnings (shown here without the directory prefix and the project-file suffix):

dotnet publishwarnings
Program.cs(3,14): warning IL2026: Using member 'OneYaml.YamlSerializer.Deserialize<T>(String, YamlSerializerOptions)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. Reflection-based binding reads members and constructors that trimming can remove. Pass a YamlTypeInfo from a YamlSerializerContext instead.
Program.cs(3,14): warning IL3050: Using member 'OneYaml.YamlSerializer.Deserialize<T>(String, YamlSerializerOptions)' which has 'RequiresDynamicCodeAttribute' can break functionality when AOT compiling. Reflection-based binding makes generic types and compiles accessors at run time. Pass a YamlTypeInfo from a YamlSerializerContext instead.

Run the published binary and the call fails at once, with a message that says what to do, rather than returning an object with missing members:

native binarystderr
Unhandled exception. System.InvalidOperationException: Reflection-based binding is turned off in this application. Pass generated metadata from a YamlSerializerContext, set TypeInfoResolver, or set the OneYaml.YamlSerializer.IsReflectionEnabledByDefault switch to true.

Reflection-based binding is off by default in apps published with PublishTrimmed or PublishAot. The MSBuild property YamlSerializerIsReflectionEnabledByDefault turns it back on, but only do that if you have kept the members it needs. Mistakes in a context, such as a context that isn’t partial, are build errors OY0001 to OY0010 (serializer).

Edit YAML files in an AOT app

Many AOT apps are command-line tools, and tools often change a config file rather than just read it. YamlDocument changes the values you set and keeps every comment and the rest of the layout. Setting strings and numbers needs no context at all:

Program.csOneYaml 1.2.1 · .NET 10
using OneYaml;
 
string yaml = """
    # Deployed by the release job
    image:
      repository: registry.example.com/billing-api
      tag: 1.4.2   # bump on release
    replicas: 3
    """;
 
using var doc = YamlDocument.Parse(yaml);
doc.Set("/image/tag", "1.5.0");
doc.Root["replicas"] = 5;
Console.WriteLine(doc);
 
string tag = doc.Root["image"]!["tag"]!.GetValue<string>();
Console.WriteLine($"tag is now {tag}");
Output
# Deployed by the release job
image:
  repository: registry.example.com/billing-api
  tag: 1.5.0   # bump on release
replicas: 5
tag is now 1.5.0

This app also published for native AOT with no warnings, and the binary prints the output above. To write a whole object into a document, YamlDocument has generated-metadata overloads: Set(path, value, typeInfo), Get(path, typeInfo) and YamlNode.Deserialize(typeInfo) (editing).

YamlDotNet and SharpYaml with native AOT

LibraryGeneratorHow you declare itSource
OneYaml 1.2.1In the OneYaml packagepartial class : YamlSerializerContext with [YamlSerializable]; pass Context.Default.Tserializer docs; publish measured above
YamlDotNet 18.1.0Separate package, Vecc.YamlDotNet.Analyzers.StaticGenerator 18.1.0, “a static context for use with YamlDotNet to support ahead-of-time and library trimming”partial class : StaticContext with [YamlStaticContext] and [YamlSerializable]; build with StaticDeserializerBuilderNuGet; issue #740 (closed as completed, March 2023); publish measured below
SharpYaml 3.16.0In the SharpYaml package (“no extra package needed”)partial class : YamlSerializerContext with [YamlSerializable]; reflection is off by default under PublishAotREADME, source generation

YamlDotNet’s wiki doesn’t cover native AOT, so this sample uses the public types of the two published packages. It ran, and it published for native AOT on linux-x64 with no warnings:

Program.csYamlDotNet 18.1.0 + StaticGenerator 18.1.0
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
 
string yaml = """
    name: billing-api
    replicas: 3
    labels:
      tier: backend
      team: payments
    """;
 
IDeserializer deserializer = new StaticDeserializerBuilder(new AppStaticContext())
    .WithNamingConvention(CamelCaseNamingConvention.Instance)
    .Build();
ServiceConfig config = deserializer.Deserialize<ServiceConfig>(yaml);
Console.WriteLine($"{config.Name}: {config.Replicas} replicas, {config.Labels.Count} labels");
 
public class ServiceConfig
{
    public string Name { get; set; } = "";
    public int Replicas { get; set; }
    public Dictionary<string, string> Labels { get; set; } = new();
}
 
[YamlStaticContext]
[YamlSerializable(typeof(ServiceConfig))]
public partial class AppStaticContext : StaticContext
{
}
Output
billing-api: 3 replicas, 2 labels

If you are moving an AOT app from YamlDotNet, note that the OneYaml.Compat.YamlDotNet package doesn’t cover YamlDotNet’s static contexts. Switch those call sites to a YamlSerializerContext as shown above. Replacing YamlDotNet covers the rest of a migration.

Where to go next

The short version of this page is the cookbook recipe use YAML in a native AOT app. To pick a library first, see choosing a C# YAML library; to change files without losing comments, edit YAML without losing comments. Install and first steps: getting started with OneYaml. Versions on this page are the latest published on 26 October 2026.