Tomlyn provides a System.Text.Json-style API for mapping TOML to .NET objects and back.
TomlSerializerTomlSerializerOptionsTomlTypeInfo<T> and TomlSerializerContext (source-generated)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);
Tomlyn can resolve object-mapping metadata in two ways:
TomlSerializerContext and TomlTypeInfo<T> - recommended for NativeAOT, trimming, and hot paths.Reflection fallback can be disabled globally before first serializer use:
AppContext.SetSwitch("Tomlyn.TomlSerializer.IsReflectionEnabledByDefault", false);
For NativeAOT or trimmed apps, disable reflection and use source-generated contexts exclusively. This ensures no trimmer warnings and optimal startup performance.
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 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);
TomlSerializerOptions is an immutable sealed record - create it once and reuse it.
TomlSerializerOptions caches internal metadata on first use. Always store and reuse options instances
rather than creating new ones per call - this avoids repeated reflection/compilation overhead.
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 | 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. |
By default, PropertyNamingPolicy is null, so CLR member names are used as-is.
This matches System.Text.Json.JsonSerializer default behavior. Common policies:
JsonNamingPolicy.CamelCase → myPropertyJsonNamingPolicy.SnakeCaseLower → my_propertyJsonNamingPolicy.KebabCaseLower → my-propertyMappingOrder 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. |
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 ('''...''').
| 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 |
Tomlyn supports common .NET collection shapes:
T[], IList<T>, List<T>, IReadOnlyList<T>ISet<T>, HashSet<T>, SortedSet<T>, IReadOnlySet<T>IDictionary<string, T>, Dictionary<string, T>, IReadOnlyDictionary<string, T>, SortedDictionary<string, T>ImmutableArray<T>, ImmutableList<T>, ImmutableDictionary<string, T>, etc.TomlTable - untyped table (key/value mapping)TomlArray - untyped arrayTomlTableArray - array of tablesobject - untyped TOML valuesSee DOM model for details.
Classes, records, and structs with public properties are mapped automatically. Constructor-based deserialization is supported (matched by parameter name to TOML key).
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.
| 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). |
| 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. |
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}";
}
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.
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; }
}
Customize type mapping by implementing TomlConverter<T> (or TomlConverterFactory for open generic types).
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());
}
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 { }
Custom converters interact with TomlReader and TomlWriter which provide efficient token-level access:
TomlReader methods:
Read() - advance to the next tokenGetString(), GetInt64(), GetDouble(), GetDecimal(), GetBoolean(), GetTomlDateTime() - read scalar valuesGetRawText() - get the original TOML textPropertyNameEquals(string) - check current property name (ordinal, case-sensitive)Skip() - skip the current value (including nested containers)TomlWriter methods:
WritePropertyName(string) - write a keyWriteStringValue(string), WriteIntegerValue(long), WriteFloatValue(double), WriteBooleanValue(bool), WriteDateTimeValue(TomlDateTime) - write scalar valuesWriteStartTable() / WriteEndTable() - write a tableWriteStartInlineTable() / WriteEndInlineTable() - write an inline tableWriteStartArray() / WriteEndArray() - write an arrayWriteStartTableArray() / WriteEndTableArray() - write an array of tablesUse 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 */;
}
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).
Tomlyn supports discriminator-based polymorphism via attributes or options.
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"
JsonPolymorphicAttribute and JsonDerivedTypeAttribute also work:
using System.Text.Json.Serialization;
[JsonPolymorphic]
[JsonDerivedType(typeof(Cat), "cat")]
[JsonDerivedType(typeof(Dog), "dog")]
public abstract class Animal { /* ... */ }
When both Toml and Json polymorphic attributes are present on the same type, the Toml-specific attributes take precedence.
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))].
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.
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:
TomlPolymorphicAttribute.UnknownDerivedTypeHandling (if not Unspecified)JsonPolymorphicAttribute.UnknownDerivedTypeHandling (mapped from JsonUnknownDerivedTypeHandling)TomlPolymorphismOptions.UnknownDerivedTypeHandling (global default)TomlUnknownDerivedTypeHandling.Unspecified is a sentinel value for attribute properties and cannot be used on TomlPolymorphismOptions - doing so throws ArgumentOutOfRangeException.
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}");
}
}
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:
Prefer TryDeserialize in scenarios where invalid TOML input is expected (e.g. user-provided configuration).
It avoids the overhead of exception throwing for expected failures.
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).