Serializing objects

edit

Objects

A class, a record or a struct is written as a table, with a key for each public property. To read it, the serializer sets the properties that have a setter (set or init), or passes the values to a constructor whose parameters match the keys by name:

var toml = TomlSerializer.Serialize(new Person("Ada", 37));
Assert.Equal("""
    Name = "Ada"
    Age = 37

    """, toml);

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

Public fields are skipped unless you set IncludeFields or mark them with [TomlInclude]. See Mapping members to rename, ignore or require members and to choose the constructor.

A TOML document is a table, so the value you serialize must be an object or a dictionary. To write a single value or a list at the root, set RootValueHandling = TomlRootValueHandling.WrapInRootKey: the value is written under the RootValueKeyName key (value by default).

Supported types

CategoryTypes
Booleanbool
Numberssbyte, byte, short, ushort, int, uint, long, ulong, nint, nuint, float, double, decimal, Half, Int128, UInt128
Textchar, string
Dates and timesDateTime, DateTimeOffset, DateOnly, TimeOnly, TomlDateTime
OtherGuid, TimeSpan, Uri, Version, enums
CollectionsT[], List<T>, IList<T>, IReadOnlyList<T>, HashSet<T>, SortedSet<T>, ISet<T>, IReadOnlySet<T>, ImmutableArray<T>, ImmutableList<T>, ImmutableHashSet<T>
DictionariesDictionary<string, T>, IDictionary<string, T>, IReadOnlyDictionary<string, T>, SortedDictionary<string, T>; ImmutableDictionary<string, T> is written but cannot be read
Document modelTomlTable, TomlArray, TomlTableArray, TomlObject, object
ObjectsClasses, records and structs

Nullable value types, such as int?, are supported. For any other type, such as Queue<T> or ConcurrentBag<T>, write a converter: the serializer rejects it with a TomlException (or the MFTOML003 error with the source generator).

Enums are written as numbers. To write their names, use TomlStringEnumConverter; see Enums as strings.

Numbers

TOML integers are signed 64-bit values. A ulong, nuint, Int128 or UInt128 value larger than long.MaxValue throws a TomlException when it is written. Floating-point values are written so that they read back to the same value.

No null

TOML has no null value. By default, a null member is not written (DefaultIgnoreCondition is WhenWritingNull), and a missing key leaves the member to its initial value. A null that must be written throws a TomlException. See Options.

Collections and arrays of tables

A collection of values is written as an array. A collection of objects is written as an array of tables, with a [[name]] header for each element:

public sealed class Config
{
    public Package[] Package { get; set; } = [];
}

public sealed class Package
{
    public string Name { get; set; } = "";
}
var toml = TomlSerializer.Serialize(new Config { Package = [new Package { Name = "core" }, new Package { Name = "tools" }] });

Assert.Equal("""
    [[Package]]
    Name = "core"

    [[Package]]
    Name = "tools"

    """, toml);

An empty collection of objects is written as an empty array (Package = []): TOML has no header for an empty array of tables. To write arrays of tables as inline arrays (Package = [{Name = "core"}]), set TableArrayStyle; see Options.

A collection member requires an array when it is read. To accept a single value too, see Single value or array.

Dictionaries

A dictionary is written as a table: its keys are the keys of the table, so they must be strings. A dictionary with another key type is rejected (MFTOML004 with the source generator). DictionaryKeyPolicy converts the keys when they are written.

A member typed as TomlTable, TomlObject or object receives the values without conversion, as described in Document model.

Dates and times

TOML has four kinds of date and time: offset date-times, local date-times, local dates and local times. The .NET types map to them:

.NET typeTOML value
DateTimeOffsetOffset date-time: 2026-10-01T08:00:00+02:00
DateTime with DateTimeKind.UtcOffset date-time with Z: 2026-10-01T08:00:00Z
DateTime with DateTimeKind.LocalOffset date-time with the offset of the machine
DateTime with DateTimeKind.UnspecifiedLocal date-time: 2026-10-01T08:00:00
DateOnlyLocal date: 2026-10-01
TimeOnlyLocal time: 08:00:00
TomlDateTimeAny kind, with the precision of the seconds
public sealed record Meeting(DateOnly Day, TimeOnly Start, DateTime CreatedAt);
var meeting = new Meeting(new DateOnly(2026, 10, 29), new TimeOnly(9, 30), new DateTime(2026, 10, 1, 8, 0, 0, DateTimeKind.Utc));

var toml = TomlSerializer.Serialize(meeting);
Assert.Equal("""
    Day = 2026-10-29
    Start = 09:30:00
    CreatedAt = 2026-10-01T08:00:00Z

    """, toml);

Assert.Equal(meeting, TomlSerializer.Deserialize<Meeting>(toml));

A DateTime is read back with the kind it was written with, like System.Text.Json. An offset other than Z is converted to local time.

A TimeSpan is written as a local time, such as 07:30:00: it is a time of day, not a duration. A negative value, or a value of 24 hours or more, throws a TomlException when it is written. Store a duration in an integer or a string member, or write a converter.

Strings, streams, readers and writers

Serialize returns a string or writes to a TextWriter; Deserialize reads a string, a TextReader or a UTF-8 Stream:

using var stream = File.OpenRead("config.toml");
var config = TomlSerializer.Deserialize<ServerConfig>(stream);

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

DeserializeAsync and SerializeAsync read and write a Stream asynchronously, for example the body of an ASP.NET Core request or response, which rejects synchronous I/O:

var config = await TomlSerializer.DeserializeAsync<ServerConfig>(request.Body, cancellationToken: cancellationToken);
await TomlSerializer.SerializeAsync(response.Body, config, cancellationToken: cancellationToken);

The asynchronous methods only make the I/O asynchronous: the whole document is still parsed and formatted in memory. Set MaxInputLength to reject documents that are too long before they are loaded; see Handling errors.