What is TOML?

TOML (Tom’s Obvious, Minimal Language) is a minimal configuration file format that’s easy to read due to obvious semantics. It maps unambiguously to hash tables and is designed to be parsed into data structures in many programming languages.

  • Version: 1.0.0

  • Extension: .toml

  • MIME Type: application/toml

  • Encoding: UTF-8

  • Case Sensitive: Yes

Basic Syntax

Comments

# This is a comment
key = "value"  # End-of-line comment

Key/Value Pairs

# Bare keys (A-Za-z0-9_-)
key = "value"
bare_key = "value"
bare-key = "value"
1234 = "value"

# Quoted keys (allow any Unicode)
"127.0.0.1" = "value"
"character encoding" = "value"
"ʎǝʞ" = "value"

# Dotted keys (grouping)
physical.color = "orange"
physical.shape = "round"
site."google.com" = true
Important
Keys cannot be defined multiple times. Bare and quoted keys are equivalent.

Data Types

Strings

# Basic strings
str = "I'm a string. \"You can quote me\"."
str_escape = "Line 1\nLine 2\tTabbed"
str_unicode = "\u03B4\U0001F4A9"

# Multi-line basic strings
multiline = """
Roses are red
Violets are blue"""

# Literal strings (no escaping)
winpath = 'C:\Users\nodejs\templates'
regex = '<\i\c*\s*>'

# Multi-line literal strings
multiline_literal = '''
The first newline is
trimmed in raw strings.
All other whitespace
is preserved.'''

Numbers

# Integers
int1 = +99
int2 = 42
int3 = 0
int4 = -17
int5 = 1_000_000  # Underscores for readability

# Hex, octal, binary
hex = 0xDEADBEEF
oct = 0o01234567
bin = 0b11010110

# Floats
flt1 = +1.0
flt2 = 3.1415
flt3 = -0.01
flt4 = 5e+22
flt5 = 1e06
flt6 = -2E-2
flt7 = 6.626e-34
flt8 = 224_617.445_991_228

# Special float values
sf1 = inf   # positive infinity
sf2 = +inf  # positive infinity
sf3 = -inf  # negative infinity
sf4 = nan   # not a number
sf5 = +nan  # not a number
sf6 = -nan  # not a number
Warning
Leading zeros are not allowed on integers.

Booleans

bool1 = true
bool2 = false
Note
Always lowercase.

Date & Time

# Offset Date-Time (RFC 3339)
odt1 = 1979-05-27T07:32:00Z
odt2 = 1979-05-27T00:32:00-07:00
odt3 = 1979-05-27T00:32:00.999999-07:00

# Local Date-Time (no timezone)
ldt1 = 1979-05-27T07:32:00
ldt2 = 1979-05-27T00:32:00.999999

# Local Date
ld1 = 1979-05-27

# Local Time
lt1 = 07:32:00
lt2 = 00:32:00.999999

Arrays

integers = [ 1, 2, 3 ]
colors = [ "red", "yellow", "green" ]
nested_arrays = [ [ 1, 2 ], [3, 4, 5] ]
nested_mixed = [ [ 1, 2 ], ["a", "b", "c"] ]

# Multi-line with trailing comma
numbers = [
  0.1, 0.2, 0.5,
  1, 2, 5,
]

# Mixed types allowed
mixed = [ 0.1, "string", true, 1979-05-27 ]

Tables

Standard Tables

[table-1]
key1 = "some string"
key2 = 123

[table-2]
key1 = "another string"
key2 = 456

# Nested tables
[dog."tater.man"]
type.name = "pug"

# Equivalent dotted keys
[servers]
alpha.ip = "10.0.0.1"
alpha.dc = "eqdc10"
beta.ip = "10.0.0.2"
beta.dc = "eqdc10"

Inline Tables

name = { first = "Tom", last = "Preston-Werner" }
point = { x = 1, y = 2 }
animal = { type.name = "pug" }

[product]
type = { name = "Nail" }
Important
Inline tables must appear on a single line and cannot be extended.

Array of Tables

[[products]]
name = "Hammer"
sku = 738594937

[[products]]  # New array element
name = "Nail"
sku = 284758393

# Nested arrays of tables
[[fruits]]
name = "apple"

  [[fruits.varieties]]
  name = "red delicious"

  [[fruits.varieties]]
  name = "granny smith"

[[fruits]]
name = "banana"

  [[fruits.varieties]]
  name = "plantain"

Best Practices

  1. Keep it simple - TOML is designed for human readability

  2. Use meaningful key names - Prefer database.port over db_p

  3. Group related settings - Use tables to organize configuration

  4. Comment your config - Explain non-obvious values

  5. Consistent formatting - Maintain uniform indentation and spacing

  6. Avoid deep nesting - Flat structures are easier to read

References

Using Tomlyn in C#

Tomlyn is the recommended C# library for working with TOML. It provides multiple approaches for interacting with TOML content.

Installation

dotnet add package Tomlyn

1. Convert TOML String to Runtime Model

1.1 To Generic Model (TomlTable)

The Toml.ToModel(string) method maps to a dynamic model with these defaults:

  • TOML table → TomlTable (implements IDictionary<string, object?>)

  • TOML table array → TomlTableArray

  • TOML array → TomlArray (implements IList<object?>)

  • Floats → C# double

  • Integers → C# long

  • Comments are preserved (via ITomlMetadataProvider)

var toml = @"global = ""this is a string""
# This is a comment of a table
[my_table]
key = 1 # Comment a key
value = true
list = [4, 5, 6]
";

// Convert TOML string to TomlTable
var model = Toml.ToModel(toml);

// Access values
Console.WriteLine(model["global"]);  // "this is a string"
Console.WriteLine(((TomlTable)model["my_table"]!)["key"]);  // 1
Console.WriteLine(string.Join(", ", (TomlArray)((TomlTable)model["my_table"]!)["list"]));  // 4, 5, 6

1.2 To Custom Model

Map TOML to strongly-typed classes using Toml.ToModel<T>(string):

var toml = @"global = ""this is a string""
# This is a comment of a table
[my_table]
key = 1 # Comment a key
value = true
list = [4, 5, 6]
";

var model = Toml.ToModel<MyModel>(toml);
Console.WriteLine($"found global = \"{model.Global}\"");
Console.WriteLine($"found key = {model.MyTable!.Key}");
Console.WriteLine($"found list = {string.Join(", ", model.MyTable!.ListOfIntegers)}");

// Model classes
class MyModel
{
    public string? Global { get; set; }
    public MyTable? MyTable { get; set; }
}

class MyTable
{
    public MyTable()
    {
        ListOfIntegers = new List<int>();
    }

    public int Key { get; set; }
    public bool Value { get; set; }

    // Property name mapping via attribute
    [DataMember(Name = "list")]
    public IList<int> ListOfIntegers { get; }

    // Ignored property
    [IgnoreDataMember]
    public string? ThisPropertyIsIgnored { get; set; }
}
Custom Model Requirements
  • Properties only - Readable-only value type properties are ignored

  • Parameter-less constructor required for class types

  • Property naming - By default, PascalCase → snake_case

    • ThisIsAnExample becomes this_is_an_example

    • Customize via TomlModelOptions.ConvertPropertyName

  • Ignore attributes - JsonIgnore, DataMemberIgnore (configurable via TomlModelOptions.AttributeListForIgnore)

  • Name attributes - JsonPropertyName, DataMember (configurable via TomlModelOptions.AttributeListForGetName)

  • Supported types - All C# primitives, string, DateTime, DateTimeOffset, TomlDateTime

  • Collections - Must inherit from ICollection<T>

  • Dictionaries - Must inherit from IDictionary<TKey, TValue>

  • Type conversion - Supports IConvertible by default (customize via TomlModelOptions.ConvertTo)

  • Instance creation - Customize via TomlModelOptions.CreateInstance(Type, ObjectKind)

Tip
Use Toml.TryToModel<T> to get a DiagnosticBag on errors without throwing exceptions.

2. Convert Runtime Model to TOML String

2.1 TomlTable to TOML String

var toml = @"global = ""this is a string""
# This is a comment of a table
[my_table]
key = 1 # Comment a key
value = true
list = [4, 5, 6]
";

var model = Toml.ToModel(toml);
var tomlOut = Toml.FromModel(model);
Console.WriteLine(tomlOut);

Output preserves comments:

global = "this is a string"
# This is a comment of a table
[my_table]
key = 1 # Comment a key
value = true
list = [4, 5, 6]

2.2 Custom Model to TOML String

var model = Toml.ToModel<MyModel>(toml);
var tomlOut = Toml.FromModel(model);
Console.WriteLine(tomlOut);

Output without comments:

global = "this is a string"
[my_table]
key = 1
value = true
list = [4, 5, 6]

2.3 Preserving Comments on Custom Model

Implement ITomlMetadataProvider to preserve comments and whitespace:

class MyModel : ITomlMetadataProvider
{
    public string? Global { get; set; }
    public MyTable? MyTable { get; set; }

    // Storage for comments and whitespace
    TomlPropertiesMetadata? ITomlMetadataProvider.PropertiesMetadata { get; set; }
}

class MyTable : ITomlMetadataProvider
{
    public MyTable()
    {
        ListOfIntegers = new List<int>();
    }

    public int Key { get; set; }
    public bool Value { get; set; }

    [DataMember(Name = "list")]
    public List<int> ListOfIntegers { get; }

    [IgnoreDataMember]
    public string? ThisPropertyIsIgnored { get; set; }

    // Storage for comments and whitespace
    TomlPropertiesMetadata? ITomlMetadataProvider.PropertiesMetadata { get; set; }
}

3. Parse and Tokenize TOML String

For low-level work (IDE tools, syntax highlighting, validators), use Toml.Parse(string) to get a DocumentSyntax tree.

3.1 Parsing to DocumentSyntax Tree

Features:

  • Exact representation including comments, whitespace, newlines, and invalid tokens

  • Can be saved even if invalid

  • Automatic validation (disable with Toml.Validate(DocumentSyntax))

var toml = @"global = ""this is a string""
# This is a comment of a table
[my_table]
key = 1 # Comment a key
value = true
list = [4, 5, 6]
";

var documentSyntax = Toml.Parse(toml);

// Check for errors
if (documentSyntax.HasErrors)
{
    foreach (var message in documentSyntax.Diagnostics)
    {
        Console.WriteLine(message);
    }
}

// Print back to string
Console.WriteLine(documentSyntax);

// Navigate syntax tree
foreach (var node in documentSyntax.Descendants())
{
    // Process nodes...
}

3.2 Fetching Tokens (Syntax Highlighting)

Extract tokens for syntax highlighting or analysis:

var input = @"# This is a comment
[table]
key = 1 # This is another comment
test.sub.key = ""yes""
[[array]]
hello = true
";

var tokens = Toml.Parse(input).Tokens().ToList();
var builder = new StringBuilder();

foreach (var node in tokens)
{
    if (node is SyntaxTrivia trivia)
    {
        builder.AppendLine($"trivia: {trivia.Span}  {trivia.Kind} " +
            $"{(trivia.Text is not null ? TomlFormatHelper.ToString(trivia.Text, TomlPropertyDisplayKind.Default) : string.Empty)}");
    }
    else if (node is SyntaxToken token)
    {
        builder.AppendLine($"token: {token.Span} " +
            $"{(token.Text is not null ? TomlFormatHelper.ToString(token.Text, TomlPropertyDisplayKind.Default) : string.Empty)}");
    }
}

Console.WriteLine(builder);

Example output:

trivia: (1,1)-(1,19)  Comment "# This is a comment"
trivia: (1,20)-(1,21)  NewLine "\r\n"
token: (2,1)-(2,1) "["
token: (2,2)-(2,6) "table"
token: (2,7)-(2,7) "]"
token: (2,8)-(2,9) "\r\n"
token: (3,1)-(3,3) "key"
trivia: (3,4)-(3,4)  Whitespaces " "
token: (3,5)-(3,5) "="
trivia: (3,6)-(3,6)  Whitespaces " "
token: (3,7)-(3,7) "1"

Common Tomlyn Patterns

Validation Only

var documentSyntax = Toml.Parse(tomlString);
var diagnostics = Toml.Validate(documentSyntax);

if (diagnostics.HasErrors)
{
    foreach (var diag in diagnostics)
    {
        Console.WriteLine($"{diag.Span}: {diag.Message}");
    }
}

Custom Property Naming

var options = new TomlModelOptions
{
    ConvertPropertyName = name => name.ToLowerInvariant()
};

var model = Toml.ToModel<MyModel>(toml, options);

Error Handling

// With exception
try
{
    var model = Toml.ToModel<MyModel>(toml);
}
catch (TomlException ex)
{
    Console.WriteLine($"TOML Error: {ex.Message}");
}

// Without exception
var result = Toml.TryToModel<MyModel>(toml, out var diagnostics);
if (!result)
{
    foreach (var diag in diagnostics)
    {
        Console.WriteLine(diag.Message);
    }
}