Table of Contents

Getting started

Install

Add the package. Its one library dependency, the shared Bodu.Text.Serialization package (the attribute family, naming policies, and callback interfaces), is restored transitively - there is nothing else to add.

dotnet add package Bodu.Text.Toml

It targets net8.0.

A first TOML round trip

using Bodu.Text.Toml;

public sealed class ServerConfig
{
    public string Host { get; set; } = "";
    public int Port { get; set; }
    public bool Secure { get; set; }
}

var config = new ServerConfig { Host = "localhost", Port = 8080, Secure = true };

string text = TomlSerializer.Serialize(config);
// Host = "localhost"
// Port = 8080
// Secure = true

ServerConfig back = TomlSerializer.Deserialize<ServerConfig>(text);

The native value model in action

TOML carries real scalar kinds, so the common BCL types land on the right wire form with no string conventions to invent. The four RFC 3339 date-time types and the float specials are the clearest illustration:

using Bodu.Text.Toml;

public sealed class Telemetry
{
    public DateTimeOffset CapturedAt { get; set; }    // offset date-time
    public DateOnly Day { get; set; }                 // local date
    public TimeOnly At { get; set; }                  // local time
    public double Ratio { get; set; }                 // float (incl. inf / nan)
    public long Samples { get; set; }                 // integer (signed 64-bit)
}

var telemetry = new Telemetry
{
    CapturedAt = new DateTimeOffset(2026, 6, 28, 9, 30, 0, TimeSpan.FromHours(10)),
    Day = new DateOnly(2026, 6, 28),
    At = new TimeOnly(9, 30, 0),
    Ratio = double.PositiveInfinity,
    Samples = 4096,
};

string text = TomlSerializer.Serialize(telemetry);
// CapturedAt = 2026-06-28T09:30:00+10:00
// Day = 2026-06-28
// At = 09:30:00
// Ratio = inf
// Samples = 4096

DateTimeOffset, DateTime (Unspecified), DateOnly, and TimeOnly map one-to-one onto offset date-time, local date-time, local date, and local time. The full per-type catalogue, including the decimal and byte[] representation choices, is in the type-mapping table and the built-in converter catalog.

Rename members

using Bodu.Text.Serialization;
using Bodu.Text.Toml;

var options = new TomlSerializerOptions
{
    PropertyNamingPolicy = NamingPolicy.SnakeCaseLower,
};

// "Host" is written as "host", "Port" as "port".
string text = TomlSerializer.Serialize(config, options);

Or pin a single member's name with [PropertyName].

Edit a document without a model

When you need to change a value but do not want a POCO, parse to the mutable DOM:

using Bodu.Text.Toml.Nodes;

TomlNode node = TomlNode.Parse(utf8Toml)!;
node["server"]!["port"] = 9090;
byte[] back = node.ToUtf8Bytes();

Read a document without a model

For inspection only, the read-only DOM is the lighter choice - a low-allocation view over the parsed buffer, walked through RootElement:

using Bodu.Text.Toml.Document;

string toml = """
[server]
host = "localhost"
port = 8080
""";

using TomlDocument doc = TomlDocument.Parse(toml);

TomlElement server = doc.RootElement.GetProperty("server");
string host = server.GetProperty("host").GetString();   // "localhost"
long   port = server.GetProperty("port").GetInt64();    // 8080

TomlDocument is disposable - wrap it in using and copy out any values that must outlive it.

Round-trip through a Stream

TomlSerializer reads and writes Stream directly, with async variants:

using Bodu.Text.Toml;

await using (FileStream stream = File.Create("server.toml"))
{
    await TomlSerializer.SerializeAsync(stream, config);
}

await using (FileStream stream = File.OpenRead("server.toml"))
{
    ServerConfig loaded = await TomlSerializer.DeserializeAsync<ServerConfig>(stream);
}

Synchronous Stream overloads are also provided.

When something goes wrong

Failures split into two exception types, so you can tell bad input apart from wrong type:

  • A malformed document - input the grammar rejects - raises TomlFormatException. Because TOML files are edited by hand, the exception carries the line, column, and offset of the failure.
  • A document that parses but cannot bind to your type - a type mismatch, a missing required member, a value the format cannot represent - raises TomlSerializationException.
try
{
    ServerConfig loaded = TomlSerializer.Deserialize<ServerConfig>(text);
}
catch (TomlFormatException ex)
{
    Console.Error.WriteLine($"Malformed TOML at line {ex.LineNumber}, column {ex.ColumnNumber}: {ex.Message}");
}
catch (TomlSerializationException ex)
{
    Console.Error.WriteLine($"Document does not match ServerConfig: {ex.Message}");
}

Where to go next