Tomlyn provides a System.Text.Json-style API for mapping TOML to .NET objects and back.

Basic usage

using Tomlyn;

public sealed record Person(string Name, int Age);

var toml = TomlSerializer.Serialize(new Person("Ada", 37));
var person = TomlSerializer.Deserialize<Person>(toml)!;

TomlSerializer provides overloads for string, Stream, and TextReader/TextWriter:

// From a file stream (UTF-8)
using var stream = File.OpenRead("config.toml");
var config = TomlSerializer.Deserialize<MyConfig>(stream);

// To a TextWriter
using var writer = new StreamWriter("output.toml");
TomlSerializer.Serialize(writer, config);

Reflection vs source-generated metadata

Tomlyn can resolve object-mapping metadata in two ways:

  1. Source-generated metadata via TomlSerializerContext and TomlTypeInfo<T> - recommended for NativeAOT, trimming, and hot paths.
  2. Reflection fallback - enabled by default; convenient for development.

Reflection fallback can be disabled globally before first serializer use:

AppContext.SetSwitch("Tomlyn.TomlSerializer.IsReflectionEnabledByDefault", false);

When reflection is disabled, POCO/object mapping requires TomlTypeInfo<T> (typically from a TomlSerializerContext). Built-in scalar converters and untyped containers (TomlTable, TomlArray) remain supported without reflection.

See Source generation and NativeAOT for full details.

Using a source-generated context

using System.Text.Json.Serialization;
using Tomlyn;
using Tomlyn.Serialization;

[JsonSerializable(typeof(MyConfig))]
internal partial class MyTomlContext : TomlSerializerContext { }

var context = MyTomlContext.Default;
var toml = TomlSerializer.Serialize(config, context.MyConfig);
var roundTrip = TomlSerializer.Deserialize(toml, context.MyConfig);

For APIs that take a Type, use overloads accepting a context:

var toml = TomlSerializer.Serialize(value, typeof(MyConfig), context);
var roundTrip = TomlSerializer.Deserialize(toml, typeof(MyConfig), context);

Options

TomlSerializerOptions is an immutable sealed record - create it once and reuse it.

using System.Text.Json;
using Tomlyn;

var options = new TomlSerializerOptions
{
    PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
    WriteIndented = true,
    IndentSize = 4,
    DefaultIgnoreCondition = TomlIgnoreCondition.WhenWritingNull,
};

var toml = TomlSerializer.Serialize(config, options);

Option reference

Option Type Default Description
PropertyNamingPolicy JsonNamingPolicy? null Naming policy for CLR member names (e.g. CamelCase, SnakeCaseLower).
DictionaryKeyPolicy JsonNamingPolicy? null Naming policy for dictionary keys during serialization.
PropertyNameCaseInsensitive bool false Case-insensitive property matching when reading.
DefaultIgnoreCondition TomlIgnoreCondition WhenWritingNull Skips null/default values when writing.
WriteIndented bool true Enables indentation for nested tables.
IndentSize int 2 Spaces per indent level.
NewLine TomlNewLineKind Lf Line ending style (Lf or CrLf).
MappingOrder TomlMappingOrderPolicy Declaration Property ordering within tables.
DuplicateKeyHandling TomlDuplicateKeyHandling Error Behavior when duplicate keys are encountered.
DottedKeyHandling TomlDottedKeyHandling Literal How dotted keys are emitted (Literal or Expand).
RootValueHandling TomlRootValueHandling Error Behavior for root-level non-table values.
RootValueKeyName string "value" Key name used when RootValueHandling is WrapInRootKey.
InlineTablePolicy TomlInlineTablePolicy Never When to emit inline tables (Never, WhenSmall, Always).
TableArrayStyle TomlTableArrayStyle Headers Array-of-tables style (Headers = [[a]], InlineArrayOfTables = a = [{...}]).
StringStylePreferences TomlStringStylePreferences (see below) Controls string emission style (basic, literal, multiline).
PolymorphismOptions TomlPolymorphismOptions (see below) Polymorphism discriminator settings.
MetadataStore ITomlMetadataStore? null Captures trivia (comments, source spans) during deserialization.
SourceName string? null File/path name included in TomlException messages.
TypeInfoResolver ITomlTypeInfoResolver? null Custom metadata resolver (advanced).
Converters IReadOnlyList<TomlConverter> empty Custom converters for type mapping.

Naming policy defaults

By default, PropertyNamingPolicy is null, so CLR member names are used as-is. This matches System.Text.Json.JsonSerializer default behavior. Common policies:

Mapping order

MappingOrder controls the order properties appear in the serialized output:

Value Behavior
Declaration Properties appear in CLR declaration order (default, diff-friendly).
Alphabetical Properties are sorted alphabetically.
OrderThenDeclaration Properties with [TomlPropertyOrder] first, then declaration order.
OrderThenAlphabetical Properties with [TomlPropertyOrder] first, then alphabetical.

String style preferences

TomlStringStylePreferences controls how strings are emitted:

var options = new TomlSerializerOptions
{
    StringStylePreferences = new TomlStringStylePreferences
    {
        DefaultStyle = TomlStringStyle.Basic,           // default
        PreferLiteralWhenNoEscapes = true,              // default
        AllowHexEscapes = true,                         // default
    }
};

The four string styles are: Basic ("..."), Literal ('...'), MultilineBasic ("""..."""), and MultilineLiteral ('''...''').

Supported types

Scalars (built-in converters)

Category Types
Boolean bool
Numeric sbyte, byte, short, ushort, int, uint, long, ulong, nint, nuint, float, double, decimal, Half, Int128, UInt128
Text char, string
Date/time DateTime, DateTimeOffset, DateOnly, TimeOnly, TomlDateTime
Other Guid, TimeSpan, Uri, Version

Collections

Tomlyn supports common .NET collection shapes:

  • Arrays: T[], IList<T>, List<T>, IReadOnlyList<T>
  • Sets: ISet<T>, HashSet<T>, SortedSet<T>, IReadOnlySet<T>
  • Dictionaries: IDictionary<string, T>, Dictionary<string, T>, IReadOnlyDictionary<string, T>, SortedDictionary<string, T>
  • Immutable collections: ImmutableArray<T>, ImmutableList<T>, ImmutableDictionary<string, T>, etc.

DOM types

See DOM model for details.

POCOs and records

Classes, records, and structs with public properties are mapped automatically. Constructor-based deserialization is supported (matched by parameter name to TOML key).

Attributes

Tomlyn supports TOML-specific attributes and a subset of System.Text.Json.Serialization attributes, so you can reuse models across JSON and TOML. When both are present, the TOML-specific attribute takes precedence.

Member-level attributes

Attribute JSON equivalent Description
TomlPropertyNameAttribute JsonPropertyNameAttribute Overrides the serialized key name.
TomlIgnoreAttribute JsonIgnoreAttribute Ignores the member. Supports conditions (Never, WhenWritingNull, WhenWritingDefault).
TomlIncludeAttribute JsonIncludeAttribute Includes non-public members.
TomlPropertyOrderAttribute JsonPropertyOrderAttribute Controls ordering within tables.
TomlRequiredAttribute JsonRequiredAttribute Member must be present in the TOML input; missing values throw TomlException.
TomlExtensionDataAttribute JsonExtensionDataAttribute Captures unmapped keys into a dictionary.
TomlConverterAttribute JsonConverterAttribute Selects a custom converter for a type or member (reflection only).

Type-level attributes

Attribute JSON equivalent Description
TomlConstructorAttribute JsonConstructorAttribute Selects which constructor to use for deserialization.
TomlPolymorphicAttribute JsonPolymorphicAttribute Enables discriminator-based polymorphism on a base type.
TomlDerivedTypeAttribute JsonDerivedTypeAttribute Registers a derived type and its discriminator value.

Example: property naming

using System.Text.Json.Serialization;

public sealed class Person
{
    [JsonPropertyName("first_name")]
    public string FirstName { get; set; } = "";

    [JsonPropertyName("last_name")]
    public string LastName { get; set; } = "";

    [JsonIgnore]
    public string FullName => $"{FirstName} {LastName}";
}

Example: required properties

using Tomlyn.Serialization;

public sealed class DatabaseConfig
{
    [TomlRequired]
    public string Host { get; set; } = "";

    [TomlRequired]
    public int Port { get; set; }

    public string? Username { get; set; }
}

If host or port is missing from the TOML input, TomlException is thrown.

Example: constructor-based deserialization

using System.Text.Json.Serialization;

public sealed class Endpoint
{
    [JsonConstructor]
    public Endpoint(string host, int port) { Host = host; Port = port; }

    public string Host { get; }
    public int Port { get; }
}

Converters

Customize type mapping by implementing TomlConverter<T> (or TomlConverterFactory for open generic types).

Implementing a converter

using Tomlyn.Serialization;

public sealed class UpperCaseStringConverter : TomlConverter<string>
{
    public override string? Read(TomlReader reader)
        => reader.GetString().ToUpperInvariant();

    public override void Write(TomlWriter writer, string value)
        => writer.WriteStringValue(value.ToLowerInvariant());
}

Registering converters

Via options (runtime):

var options = new TomlSerializerOptions
{
    Converters = [new UpperCaseStringConverter()]
};

Via attribute (reflection only):

public sealed class Config
{
    [TomlConverter(typeof(UpperCaseStringConverter))]
    public string Name { get; set; } = "";
}

Via source generation (compile-time):

[TomlSourceGenerationOptions(Converters = [typeof(UpperCaseStringConverter)])]
[JsonSerializable(typeof(Config))]
internal partial class MyTomlContext : TomlSerializerContext { }

TomlReader and TomlWriter

Custom converters interact with TomlReader and TomlWriter which provide efficient token-level access:

TomlReader methods:

  • Read() - advance to the next token
  • GetString(), GetInt64(), GetDouble(), GetDecimal(), GetBoolean(), GetTomlDateTime() - read scalar values
  • GetRawText() - get the original TOML text
  • PropertyNameEquals(string) - check current property name (ordinal, case-sensitive)
  • Skip() - skip the current value (including nested containers)

TomlWriter methods:

  • WritePropertyName(string) - write a key
  • WriteStringValue(string), WriteIntegerValue(long), WriteFloatValue(double), WriteBooleanValue(bool), WriteDateTimeValue(TomlDateTime) - write scalar values
  • WriteStartTable() / WriteEndTable() - write a table
  • WriteStartInlineTable() / WriteEndInlineTable() - write an inline table
  • WriteStartArray() / WriteEndArray() - write an array
  • WriteStartTableArray() / WriteEndTableArray() - write an array of tables

Converter factories

Use TomlConverterFactory for open generic types or types that need runtime inspection:

using Tomlyn.Serialization;

public sealed class MyConverterFactory : TomlConverterFactory
{
    public override bool CanConvert(Type typeToConvert)
        => typeToConvert.IsEnum;

    public override TomlConverter CreateConverter(Type typeToConvert, TomlSerializerOptions options)
        => /* return a concrete TomlConverter<T> for the enum type */;
}

Extension data

Capture unmapped TOML keys during deserialization using TomlExtensionDataAttribute (or JsonExtensionDataAttribute):

using Tomlyn.Serialization;

public sealed class Config
{
    public string Name { get; set; } = "";

    [TomlExtensionData]
    public IDictionary<string, object?>? Extra { get; set; }
}

Any key in the TOML input that doesn't match a mapped property is placed in Extra. The extension data member should be a dictionary type with string keys (e.g. IDictionary<string, object?>, TomlTable).

Polymorphism

Tomlyn supports discriminator-based polymorphism via attributes or options.

Attribute-based

using Tomlyn.Serialization;

[TomlPolymorphic]
[TomlDerivedType(typeof(Cat), "cat")]
[TomlDerivedType(typeof(Dog), "dog")]
public abstract class Animal
{
    public string Name { get; set; } = "";
}

public sealed class Cat : Animal { public bool Indoor { get; set; } }
public sealed class Dog : Animal { public string Breed { get; set; } = ""; }

The discriminator key defaults to $type and can be customized via TomlPolymorphicAttribute.TypeDiscriminatorPropertyName or TomlPolymorphismOptions.TypeDiscriminatorPropertyName on options.

TOML input:

[[ animals ]]
"$type" = "cat"
Name = "Whiskers"
Indoor = true

[[ animals ]]
"$type" = "dog"
Name = "Rex"
Breed = "Labrador"

JSON attribute equivalents

JsonPolymorphicAttribute and JsonDerivedTypeAttribute also work:

using System.Text.Json.Serialization;

[JsonPolymorphic]
[JsonDerivedType(typeof(Cat), "cat")]
[JsonDerivedType(typeof(Dog), "dog")]
public abstract class Animal { /* ... */ }

Default derived type

Register one derived type without a discriminator to act as the default. When a TOML table has no discriminator key (or an unknown discriminator), it deserializes as the default type. When serializing the default type, no discriminator is emitted.

[TomlPolymorphic(TypeDiscriminatorPropertyName = "type")]
[TomlDerivedType(typeof(Circle))]         // default - no discriminator
[TomlDerivedType(typeof(Square), "square")]
public abstract class Shape
{
    public string Color { get; set; } = "";
}

public sealed class Circle : Shape { public double Radius { get; set; } }
public sealed class Square : Shape { public double Side { get; set; } }
# Serialized Circle (default) - no "type" key
Color = "red"
Radius = 5.0

# Serialized Square - "type" key is present
type = "square"
Color = "blue"
Side = 3.0

The same works with JsonDerivedTypeAttribute using the single-argument constructor [JsonDerivedType(typeof(Circle))].

Integer discriminators

In addition to strings, TomlDerivedTypeAttribute accepts an int discriminator which is stored as a string in TOML:

[TomlPolymorphic(TypeDiscriminatorPropertyName = "type")]
[TomlDerivedType(typeof(Circle), 1)]
[TomlDerivedType(typeof(Square), 2)]
public abstract class Shape { /* ... */ }
type = "1"
Color = "red"
Radius = 5.0

JsonDerivedTypeAttribute integer discriminators (e.g. [JsonDerivedType(typeof(Circle), 1)]) are also supported.

Unknown discriminator handling

By default, encountering an unrecognized discriminator throws a TomlException. You can change this behavior at the attribute level or globally.

Attribute-level (takes priority):

[TomlPolymorphic(
    TypeDiscriminatorPropertyName = "kind",
    UnknownDerivedTypeHandling = TomlUnknownDerivedTypeHandling.FallBackToBaseType)]
[TomlDerivedType(typeof(Cat), "cat")]
public class Animal
{
    public string Name { get; set; } = "";
}

Global options:

var options = new TomlSerializerOptions
{
    PolymorphismOptions = new TomlPolymorphismOptions
    {
        UnknownDerivedTypeHandling = TomlUnknownDerivedTypeHandling.FallBackToBaseType,
    },
};

The priority chain is:

  1. TomlPolymorphicAttribute.UnknownDerivedTypeHandling (if not Unspecified)
  2. JsonPolymorphicAttribute.UnknownDerivedTypeHandling (mapped from JsonUnknownDerivedTypeHandling)
  3. TomlPolymorphismOptions.UnknownDerivedTypeHandling (global default)

Serialization callbacks

Implement callback interfaces on your types to run logic before/after serialization or deserialization:

Interface Method When called
ITomlOnSerializing OnTomlSerializing() Before the object is serialized.
ITomlOnSerialized OnTomlSerialized() After the object is serialized.
ITomlOnDeserializing OnTomlDeserializing() Before the object's properties are populated.
ITomlOnDeserialized OnTomlDeserialized() After the object's properties are populated.
using Tomlyn.Serialization;

public sealed class Config : ITomlOnDeserialized
{
    public string Host { get; set; } = "localhost";
    public int Port { get; set; } = 8080;

    public void OnTomlDeserialized()
    {
        if (Port is < 1 or > 65535)
            throw new InvalidOperationException($"Invalid port: {Port}");
    }
}

Error handling

Parsing and mapping errors throw TomlException with source location details.

Key properties on TomlException:

Property Type Description
SourceName string? File/path name (set via TomlSerializerOptions.SourceName).
Line int? 1-based line number.
Column int? 1-based column number.
Offset int? 0-based character offset.
Span TomlSourceSpan? Full start/end span.
Diagnostics DiagnosticsBag All collected diagnostics.

For a non-throwing API, use TryDeserialize:

if (!TomlSerializer.TryDeserialize<MyConfig>(toml, out var config))
{
    // config is null/default - parsing or mapping failed
}

TryDeserialize is available for all input types (string, Stream, TextReader) and with all metadata styles (options, context, type info).