dotnet add package Tomlyn
Tomlyn ships builds for net8.0, net10.0, and netstandard2.0.
The main entry point is TomlSerializer - it works just like System.Text.Json.JsonSerializer:
using Tomlyn;
public sealed class ServerConfig
{
public string Host { get; set; } = "localhost";
public int Port { get; set; } = 8080;
public bool Ssl { get; set; }
}
// Serialize to TOML
var config = new ServerConfig { Host = "example.com", Port = 443, Ssl = true };
var toml = TomlSerializer.Serialize(config);
// Deserialize from TOML
var roundTrip = TomlSerializer.Deserialize<ServerConfig>(toml)!;
Output:
Host = "example.com"
Port = 443
Ssl = true
By default TomlSerializerOptions.DefaultIgnoreCondition is set to TomlIgnoreCondition.WhenWritingNull,
so null properties are omitted from the output.
By default, CLR member names are used as-is. Use PropertyNamingPolicy for snake_case or camelCase keys:
using System.Text.Json;
using Tomlyn;
var options = new TomlSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
};
var toml = TomlSerializer.Serialize(new ServerConfig { Host = "example.com", Port = 443 }, options);
// host = "example.com"
// port = 443
Stream overloads are for convenience. Tomlyn reads the entire stream into memory before parsing (see Performance).
Tomlyn provides Stream and TextReader overloads to avoid manual StreamReader/StreamWriter boilerplate:
using System.IO;
using Tomlyn;
// Read from a file stream
using var input = File.OpenRead("config.toml");
var config = TomlSerializer.Deserialize<ServerConfig>(input)!;
// Write to a file stream
using var output = File.Create("config_out.toml");
TomlSerializer.Serialize(output, config);
TomlSerializerOptions is an immutable sealed record - create it once and reuse it:
using System.Text.Json;
using Tomlyn;
var options = new TomlSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = true,
IndentSize = 2,
DefaultIgnoreCondition = TomlIgnoreCondition.WhenWritingNull,
DuplicateKeyHandling = TomlDuplicateKeyHandling.Error,
};
var toml = TomlSerializer.Serialize(config, options);
var roundTrip = TomlSerializer.Deserialize<ServerConfig>(toml, options);
See Serialization for the full option reference table.
When parsing or mapping fails, Tomlyn throws TomlException with precise source locations:
try
{
var config = TomlSerializer.Deserialize<ServerConfig>(
"bad = [",
new TomlSerializerOptions { SourceName = "config.toml" });
}
catch (TomlException ex)
{
// ex.SourceName → "config.toml"
// ex.Line → 1
// ex.Column → 7
Console.WriteLine(ex.Message);
}
If you prefer a non-throwing API, use TryDeserialize:
if (!TomlSerializer.TryDeserialize<ServerConfig>(toml, out var value))
{
// value is null/default when parsing fails
}
If you don't want to define classes, deserialize into TomlTable:
using Tomlyn;
using Tomlyn.Model;
var toml = """
title = "My App"
[database]
host = "localhost"
port = 5432
""";
var table = TomlSerializer.Deserialize<TomlTable>(toml)!;
var title = (string)table["title"]!; // "My App"
var db = (TomlTable)table["database"]!;
var port = (long)db["port"]!; // 5432
See DOM model for more details.
For NativeAOT / trimming, declare a TomlSerializerContext and use generated metadata:
using System.Text.Json.Serialization;
using Tomlyn;
using Tomlyn.Serialization;
[JsonSerializable(typeof(ServerConfig))]
internal partial class MyTomlContext : TomlSerializerContext { }
var toml = TomlSerializer.Serialize(config, MyTomlContext.Default.ServerConfig);
var roundTrip = TomlSerializer.Deserialize(toml, MyTomlContext.Default.ServerConfig);
See Source generation and NativeAOT for details.
TomlTable / TomlArray manipulation.