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
-
Keep it simple - TOML is designed for human readability
-
Use meaningful key names - Prefer
database.portoverdb_p -
Group related settings - Use tables to organize configuration
-
Comment your config - Explain non-obvious values
-
Consistent formatting - Maintain uniform indentation and spacing
-
Avoid deep nesting - Flat structures are easier to read
References
-
Official Spec: https://toml.io/en/v1.0.0
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(implementsIDictionary<string, object?>) -
TOML table array →
TomlTableArray -
TOML array →
TomlArray(implementsIList<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-
ThisIsAnExamplebecomesthis_is_an_example -
Customize via
TomlModelOptions.ConvertPropertyName
-
-
Ignore attributes -
JsonIgnore,DataMemberIgnore(configurable viaTomlModelOptions.AttributeListForIgnore) -
Name attributes -
JsonPropertyName,DataMember(configurable viaTomlModelOptions.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
IConvertibleby default (customize viaTomlModelOptions.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);
}
}