Tomlyn provides several ways to interact with TOML content:
- 1. Convert a TOML string to a runtime model
- 2. Convert a runtime model to TOML string
- 3. Parse and tokenize a TOML string
Use this approach if you are looking to access the data with convenient accessors.
See Parse and tokenize a TOML string if you are requiring to have precise processing of the original TOML document.
The default Toml.ToModel(string)
method provides a convenient way to map to a dynamic model.
This mapping comes with the following default handling:
- A TOML table maps to a
TomlTable
object and is in practice aIDictionary<string, object?>
. - A TOML table array maps to a
TomlTableArray
object - A TOML array maps to a
TomlArray
object and is in practice aIList<object?>
. - Floats/Double are all mapped to C#
double
. - Integers are all mapped to C#
long
. - Comments are preserved by default as
TomlTable
is implementingITomlMetadataProvider
(See Convert a runtime model to TOML string for more details).
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]
";
// Converts the TOML string to a `TomlTable`
var model = Toml.ToModel(toml);
// Prints "this is a string"
Console.WriteLine(model["global"]);
// Prints "1"
Console.WriteLine(((TomlTable)model["my_table"]!)["key"]);
// Prints 4, 5, 6
Console.WriteLine(string.Join(", ", (TomlArray)((TomlTable)model["my_table"]!)["list"]));
Tomlyn allows to map to a TOML string to a custom runtime model by using System.Reflection
via the method Toml.ToModel<T>(string)
.
The mapping comes with a few requirements and is highly customizable:
- Maps properties only.
- Readable only properties for string/value types are ignored.
- Can ignore properties via attributes (
JsonIgnore
,DataMemberIgnore
). These attributes can be configured inTomlModelOptions.AttributeListForIgnore
- By default property names are lowered and split by
_
by PascalCase letters- For example:
ThisIsAnExample
becomesthis_is_an_example
- This behavior can be changed by passing a
TomlModelOptions
and specifying theTomlModelOptions.ConvertPropertyName
delegate. - If the property contains a special attribute (e.g
JsonPropertyName
,DataMember
), it will use this name instead. You can control the list of attributes inTomlModelOptions.AttributeListForGetName
.
- For example:
- It is possible to override the lowering of a property at a lower level via
TomlModelOptions.GetPropertyName
delegate. Note that this delegate is handling the renaming by attributes and calling the configuredTomlModelOptions.ConvertPropertyName
.
- Requires class types to have a parameter-less constructor.
- Supports all C# primitive types,
string
,DateTime
,DateTimeOffset
andTomlDateTime
. - Prefer concrete types instead of interfaces or abstract types.
- Unless you can guarantee that your model provides an instantiation of the interface (e.g see example below with the
MyTable.ListOfIntegers
property returning anIList<int>
) - You can customize instances created by setting a delegate on
TomlModelOptions.CreateInstance(Type, ObjectKind)
.
- Unless you can guarantee that your model provides an instantiation of the interface (e.g see example below with the
- Conversion between types supports
IConvertible
by default- You can customize the conversion by setting a delegate on
TomlModelOptions.ConvertTo(object value, Type toType)
- You can customize the conversion by setting a delegate on
- Collections used in the model must inherit from
ICollection<T>
- Dictionary used in the model must inherit from
IDictionary<TKey, TValue>
NOTE: if the model is not able to map properties, it will throw a
TomlException
detailing the missing properties. You can useToml.TryToModel<T>
which returns aDiagnosticBag
in case of an error without throwing an exception.
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);
// Prints "this is a string"
Console.WriteLine($"found global = \"{model.Global}\"");
// Prints 1
var key = model.MyTable!.Key;
Console.WriteLine($"found key = {key}");
// Check list
var list = model.MyTable!.List;
Console.WriteLine($"found list = {string.Join(", ", list)}");
// Simple model that maps the TOML string above
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; }
// The type can be an interface if it is pre-instantiated by this instance.
[DataMember(Name = "list")]
public IList<int> ListOfIntegers { get; }
[IgnoreDataMember]
public string? ThisPropertyIsIgnored {get; set;}
}
You can easily write back a model to a TOML string by using the method Toml.FromModel(object model)
.
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);
This will print the original TOML by preserving most the 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]
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);
var tomlOut = Toml.FromModel(model);
Console.WriteLine(tomlOut);
This will print the TOML without preserving comments:
global = "this is a string"
[my_table]
key = 1
value = true
list = [4, 5, 6]
TomlTable
is able to preserve the comments by implementing the interface ITomlMetadataProvider
(here).
You can also preserve comments and whitespace with a custom model by implementing this interface. You just need to explicitly implement the interface:
public class MyModel : ITomlMetadataProvider
{
// storage for comments and whitespace
TomlPropertiesMetadata? ITomlMetadataProvider.PropertiesMetadata { get; set; }
// ...
For example comments can be preserved on the previous model by adding the interface:
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; }
}
It is sometimes require to work at lower level for authoring tools (IDE, syntax highlighting, syntax validator...etc.) via the method Toml.Parse(string)
.
Tomlyn provides an a DocumentSyntax
API to work directly on the syntax tree:
- Provides an exact representation of the original document, including comments, whitespace, newlines and invalid characters/tokens.
- Can be saved back to disk even if the TOML document is invalid.
- The document is validated with a TOML validator. You can disable the validator or run it separately via
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);
// Prints any errors on the syntax
if (documentSyntax.HasErrors) {
foreach(var message in documentSyntax.Diagnostics) {
Console.WriteLine(message);
}
}
// Prints back the model to string
Console.WriteLine(documentSyntax);
The syntax tree provides convenient methods to navigate the syntax tree via the method syntax.Descendants()
.
If you want to fetch tokens (e.g for syntax highlighting) you can parse to a DocumentSyntax
and call the extension method DocumentSyntax.Tokens()
.
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);
It will print the following details:
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"
trivia: (3,8)-(3,8) Whitespaces " "
trivia: (3,9)-(3,33) Comment "# This is another comment"
token: (3,34)-(3,35) "\r\n"
token: (4,1)-(4,4) "test"
token: (4,5)-(4,5) "."
token: (4,6)-(4,8) "sub"
token: (4,9)-(4,9) "."
token: (4,10)-(4,12) "key"
trivia: (4,13)-(4,13) Whitespaces " "
token: (4,14)-(4,14) "="
trivia: (4,15)-(4,15) Whitespaces " "
token: (4,16)-(4,20) "\"yes\""
token: (4,21)-(4,22) "\r\n"
token: (5,1)-(5,2) "[["
token: (5,3)-(5,7) "array"
token: (5,8)-(5,9) "]]"
token: (5,10)-(5,11) "\r\n"
token: (6,1)-(6,5) "hello"
trivia: (6,6)-(6,6) Whitespaces " "
token: (6,7)-(6,7) "="
trivia: (6,8)-(6,8) Whitespaces " "
token: (6,9)-(6,12) "true"
token: (6,13)-(6,14) "\r\n"