# Tomlyn makes TOML 1.1 look like System.Text.Json

> Tomlyn is a C# TOML parser, validator and authoring library from xoofx that deliberately targets TOML 1.1 only and mirrors the System.Text.Json API surface, with source generation for NativeAOT and a lossless syntax tree underneath. The choice to drop TOML 1.0 support is the detail to read before you adopt it.

**xoofx/Tomlyn** — Tomlyn is a TOML parser, validator and authoring library for .NET Framework and .NET Core

- Repository: https://github.com/xoofx/Tomlyn
- Website: https://xoofx.github.io/Tomlyn/
- Stars: 600 · Forks: 46
- Language: C#
- License: BSD-2-Clause
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/xoofx-tomlyn

## The API is deliberately System.Text.Json

Tomlyn v1 is described as a major redesign with breaking changes from earlier versions, and the headline of that redesign is that the surface looks like the JSON library most .NET developers already use. The names are TomlSerializer, TomlSerializerOptions and a resolver-based metadata abstraction, ITomlTypeInfoResolver, and there is a TomlTypeInfo<T> alongside it. The quick start is two calls.

```csharp
using Tomlyn;

// Serialize
var toml = TomlSerializer.Serialize(new { Name = "Ada", Age = 37 });

// Deserialize
var person = TomlSerializer.Deserialize<Person>(toml);
```

The point of copying a familiar API is not cosmetic. A team that already reasons about JsonNamingPolicy, JsonObjectCreationHandling and JsonPropertyName can reason about Tomlyn without a new mental model, and the interop list is specific about which System.Text.Json attributes are reused, among them JsonPropertyName, JsonIgnore, JsonRequired, JsonConstructor, JsonObjectCreationHandling and the polymorphism attributes. The default behaviour matches too, since PropertyNamingPolicy is null by default and CLR member names are used as-is for TOML mapping keys, the same default as System.Text.Json.

## TOML 1.1 only, and that is a hard boundary

This is the constraint to check first, because it is the kind of thing that surfaces as a parse error in production rather than as a build failure. The features list states that Tomlyn v1 targets TOML 1.1.0 and does not support TOML 1.0. If your existing TOML files follow the 1.0 specification, some constructs will not parse, and the library will not silently accept them for you. The specification is still a draft in the eyes of the library author, which is why the version gap exists at all. A migration guide is referenced in the README for the move to the new API, and the project is at version 2.10.1 in the release list, with 2.9.0 and 2.8.0 released in June 2026, so the 1.x line referred to in the note is a different numbering from the current 2.x packages. The relevant fact for a new adopter is narrower: check the TOML version your files are written against before you wire the library in.

## Options mirror the JSON options, including the defaults

The options object is the second half of the familiarity argument, and the documentation is careful about the defaults, which is where these libraries usually surprise people.

```csharp
var options = new TomlSerializerOptions
{
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
    PreferredObjectCreationHandling = JsonObjectCreationHandling.Replace,
    WriteIndented = true,
    IndentSize = 4,
    MaxDepth = 64,
    DefaultIgnoreCondition = TomlIgnoreCondition.WhenWritingNull,
};
```

Two defaults are called out in prose. MaxDepth of 0 uses the built-in default of 64, and PreferredObjectCreationHandling of Replace matches System.Text.Json, meaning read-only properties are not populated unless you opt into Populate through the options or through the JsonObjectCreationHandling attribute. That second one catches people out, because a configuration object with get-only properties will silently come back empty unless you say otherwise. The naming policy and the ignore condition behave as you would expect from the JSON side, and IndentSize controls the output layout. The author note in the documentation is that read-only properties are not populated by default, which is the JSON behaviour people already know, but it is worth reading before you point this at an existing config class.

## Source generation is the NativeAOT path

For anyone shipping a .NET application with trimming or NativeAOT enabled, the source generation story is the reason to pick this library over a reflection-based TOML parser. A context class is declared with the TomlSerializable attribute, and the source generated metadata is then passed to the serializer.

```csharp
[TomlSourceGenerationOptions(
    PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
    PreferredObjectCreationHandling = JsonObjectCreationHandling.Replace)]
[TomlSerializable(typeof(MyConfig))]
internal partial class MyTomlContext : TomlSerializerContext
{
}

var config = TomlSerializer.Deserialize(toml, MyTomlContext.Default.MyConfig);
```

The source generator ships inside the package under analyzers/dotnet/cs, so there is no extra package to install. Reflection based POCO mapping is still available, and the documentation says it can be disabled for NativeAOT through a feature switch or an MSBuild property, which is the switch you flip when the trimming analyser complains. That combination of a first class generated path and a reflection path you can turn off is what makes the library usable in both a server application and a trimmed desktop or native binary. The trade-off is the usual one. A source generated context means you add an attribute for every root type and you regenerate when the model changes.

## Polymorphism, single-or-array and formatting attributes

Three features exist because plain TOML cannot express what config files actually need. Cross-project polymorphism covers the case where a base type lives in one assembly and derived types live in another, and the derived types can be registered at runtime on a TomlPolymorphismOptions instance or on a source generated context. Base type registrations still take precedence when the same discriminator or derived type is registered more than once, and the mapping API is additive rather than a replacement. The single-or-array collection opt-in is the one config file writers hit constantly, where a field is written as a bare value in one file and as an array in another. The TomlSingleOrArray attribute on a collection member makes both payloads valid, and the example shows it combined with a JsonPropertyName so the TOML key stays short. On the output side there are TOML formatting attributes that let members and types opt into table-array, inline-table, string, mapping-order and dotted-key formatting policies, which matters when the file has to be read by another tool that expects a particular layout.

## Underneath: a lexer, a parser and a lossless tree

The library is not only a serializer, and the low-level half is what makes it a parser rather than a mapper. There is an allocation-free parsing pipeline with an incremental TomlLexer feeding a TomlParser, and the parser produces precise spans so error messages can point at a location rather than at the file. Above that sits a lossless, trivia-preserving syntax tree, produced by SyntaxParser into a DocumentSyntax. Trivia preservation is the property that matters, because it means comments and formatting survive a round trip, so a file can be parsed and written back with the parts Tomlyn does not understand left alone. There is also a full lexer and parser API for callers who need to work at that level, and an untyped model where a document is deserialized into a TomlTable that you index by key, which is the escape hatch for a config format you do not have a C# class for. A TomlObject member can accept any TOML model container, whether a table, an array or a table array.

## Target frameworks and the cost of building from source

The requirement list is short and worth reading carefully. Tomlyn targets net8.0, net10.0 and netstandard2.0, and consuming the NuGet package works on any runtime that supports netstandard2.0, which includes .NET Framework, or on modern .NET at 8.0 and above. So the install step is a single package reference and the library runs on the old .NET Framework stack as well as current .NET.

```sh
dotnet add package Tomlyn
```

The wrinkle is on the contributor side. Building Tomlyn from source requires the .NET 10 SDK, while consuming the package does not. For an application team that is irrelevant, and for a team that wants to patch the library, or a supply chain policy that requires building from source, it is a hard floor. The repository layout is small and tells you the shape of the project, with src/ holding the library, ext/ for submodules, site/ for the documentation site, an .alta/ directory, and both AGENTS.md and CLAUDE.md at the root. The licence is BSD-2-Clause, recorded in license.txt, and the CI and coveralls badges in the README show the build and coverage are wired to the same repository.

## Conclusion

Tomlyn suits .NET teams that want TOML handled by a library shaped like the serializer they already use, and that need NativeAOT friendly source generated metadata rather than reflection. It does not suit projects whose TOML files are written to the 1.0 specification, since the library does not support 1.0, and it does not suit a team that needs YAML. Before adopting it, run dotnet add package Tomlyn and confirm the version you pull targets TOML 1.1, read the migration guide because version 1 was a breaking redesign, and decide whether the .NET 10 SDK requirement for building from source affects your build pipeline.

## FAQ

### Does Tomlyn support TOML 1.0?

No. The features list states that Tomlyn v1 targets TOML 1.1.0 and does not support TOML 1.0, so files written to the older specification may not parse.

### How do I install Tomlyn?

Run dotnet add package Tomlyn. The source generator ships inside the package, so no extra package is needed, and the package works on any runtime supporting netstandard2.0 or modern .NET at 8.0 and above.

### How do I use Tomlyn with NativeAOT?

Use source generation by declaring a TomlSerializerContext with TomlSerializable attributes, then pass the generated context to TomlSerializer. Reflection based mapping is still available but can be disabled for NativeAOT through a feature switch or an MSBuild property.

### Can Tomlyn handle a field that is sometimes a value and sometimes an array?

Yes. The TomlSingleOrArray attribute on a collection member makes both a single TOML value and an array valid input, which is a common shape in configuration files.

### What licence is Tomlyn released under?

Tomlyn is released under the BSD-2-Clause licence, recorded in the license.txt file at the top of the repository.

## Sources

- [License: BSD-2-Clause](https://github.com/xoofx/Tomlyn/blob/main/LICENSE)
- [Project website](https://xoofx.github.io/Tomlyn/)
- [README](https://github.com/xoofx/Tomlyn/blob/main/README.md)
- [Releases](https://github.com/xoofx/Tomlyn/releases)
- [xoofx/Tomlyn on GitHub](https://github.com/xoofx/Tomlyn)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/xoofx-tomlyn
