TOML (Tom’s Obvious, Minimal Language) is a straightforward configuration file format designed for human readability. It is widely used in various projects to manage application settings. The format organizes data using key structures such as arrays, tables, and subtables, making it both simple and efficient for configuration management.
In this blog post, we’ll break down how to use arrays of tables, subtables, and inline tables in TOML. We’ll explore some common mistakes, how TOML ensures data consistency, and how you can use these features effectively.
What are Arrays of Tables in TOML?
In TOML, a table is a collection of key-value pairs, similar to a dictionary or JSON object. An array of tables is a list of such tables, each having the same structure but different values.
This is useful when defining multiple items of the same type, such as a list of users, servers, configurations, or even a collection of products, fruits, or other items.
Syntax
Arrays of tables are defined using double square brackets [[ ... ]]
. Each set of double square brackets creates a new table in the array.
[[servers]]
host = "192.168.1.1"
port = 8080
[[servers]]
host = "192.168.1.2"
port = 9090
Here,
- We define an array of tables named
servers
. - Each
[[servers]]
block represents an entry in the array. - Each server has a
host
andport
.
The resulting structure in JSON would look like this:
{
"servers": [
{ "host": "192.168.1.1", "port": 8080 },
{ "host": "192.168.1.2", "port": 9090 }
]
}
One more simple example for clarity.
[[fruit]]
name = "Apple"
color = "Red"
[[fruit]]
name = "Banana"
color = "Yellow"
[[fruit]]
name = "Grapes"
color = "Green"
In JSON, this would look like:
{
"fruit": [
{ "name": "Apple", "color": "Red" },
{ "name": "Banana", "color": "Yellow" },
{ "name": "Grapes", "color": "Green" }
]
}
How It Works
Arrays of tables begin with the first occurrence of that header, which initializes the array and adds its first table element. Each subsequent occurrence creates and adds a new table element to the array, with tables inserted in the order they appear.
Means,
- First instance of
[[table_name]]
→ Defines the array and its first table element. - Subsequent instances of
[[table_name]]
→ Add more tables to the array. - Insertion order matters → The tables appear in the order they are written.
[[products]]
name = "Apple MacBook Air (M2)"
sku = 7393945837
[[products]] # empty table within the array
[[products]]
name = "Asus Zenbook 14X OLED"
sku = 393857482
color = "space gray" # For readability and clarity, a space was added above, but it is part of the same products table (which contains 'name' as 'Asus Zenbook 14X OLED')
Here,
[[products]]
: This creates the first table inside an array calledproducts
. The table has two keys:name
andsku
.[[products]]
(Empty Table): This is an empty table inside the sameproducts
array. No key-value pairs are defined here.[[products]]
(Another Table): This creates another table inside the same array. It hasname
,sku
, andcolor
as keys.
If you convert this TOML structure to JSON, it would look like this:
{
"products": [
{ "name": "Apple MacBook Air (M2)", "sku": 7393945837 },
{ },
{ "name": "Asus Zenbook 14X OLED", "sku": 393857482, "color": "gray" }
]
}
Notice how JSON represents the array of tables. Each object in the array corresponds to a table in TOML, and the empty table is represented as an empty object {}
in JSON.
Note :
- The
[[products]]
syntax is used to define an array of tables, with each table being a separate entry in the array. - The tables are ordered by their appearance, and you can have empty tables within the array as well.
Nested Arrays of Tables
First, let’s do a quick recap. As we know, we can define subtables within a table, allowing us to group related data under a single parent table. A subtable is essentially a table within a table and is indicated using dot notation ([parent.subtable]).
Now, when we reference an array of tables, it always refers to the most recently defined table element in that array. This behavior allows us to define subtables (nested tables) and even sub-arrays of tables (nested arrays of tables) within the last added table.
Let’s understand this:
- Each
[[array_name]]
creates a new table inside an array. - Any sub-table (
[array_name.subtable]
) or sub-array ([[array_name.subtable]]
) added immediately after a[[array_name]]
entry applies to the most recently created table.
Example 1: Defining Sub-Tables
[[company]]
name = "TechCorp"
[company.address]
city = "New York"
zip = "10001"
[[company]]
name = "softAai"
[company.address]
city = "Pune"
zip = "411001"
Equivalent JSON Representation:
{
"company": [
{
"name": "TechCorp",
"address": {
"city": "New York",
"zip": "10001"
}
},
{
"name": "softAai",
"address": {
"city": "Pune",
"zip": "411001"
}
}
]
}
Here, in the above TOML file,
[[company]]
defines an array of tables namedcompany
.- The first instance (
TechCorp
) gets a sub-tablecompany.address
with"New York"
. - The second instance (
softAai
) gets a differentcompany.address
with"Pune"
. - The
company.address
sub-table always applies to the last[[company]]
entry. As you can see, it belongs to the most recently added or created table instance, which is why it is part of the second element (softAai
) in the array of tables.
Example 2: Defining a Sub-Array of Tables
[[library]]
name = "Pune University Library"
[[library.books]]
title = "The Guide"
author = "R.K. Narayan"
[[library.books]]
title = "Malgudi Days"
author = "R.K. Narayan"
[[library]]
name = "Pune City Library"
[[library.books]]
title = "Godan"
author = "Munshi Premchand"
Equivalent JSON Representation:
{
"library": [
{
"name": "Pune University Library",
"books": [
{
"author": "R.K. Narayan",
"title": "The Guide"
},
{
"author": "R.K. Narayan",
"title": "Malgudi Days"
}
]
},
{
"name": "Pune City Library",
"books": [
{
"author": "Munshi Premchand",
"title": "Godan"
}
]
}
]
}
Here, in the above TOML file,
[[library]]
defines an array of libraries.- Each
[[library.books]]
entry belongs to the most recent[[library]]
. "Pune University Library"
has two books, while"Pune City Library"
has one book.- This results in a nested array of tables, which means you can have tables inside an array that itself contains another table.
Let’s look at a slightly more complex example, where we combine sub-tables and nested arrays of tables.
[[fruits]]
name = "mango"
[fruits.physical] # Subtable for the physical attributes of the fruit
color = "yellow"
shape = "oval"
[[fruits.varieties]] # Nested array of tables for the varieties of the fruit
name = "alphonso"
[[fruits.varieties]]
name = "dasheri"
[[fruits]]
name = "banana"
[[fruits.varieties]]
name = "robusta"
Here,
- The first
[[fruits]]
creates a table for a mango, with aname
field. - The
[fruits.physical]
subtable defines physical properties of the mango, like its color and shape. It’s nested within thefruits
table. - The
[[fruits.varieties]]
creates an array of tables to list different varieties of the mango, such as “alphonso” and “dasheri”. - Another
[[fruits]]
is added for a banana, which also has varieties defined.
Equivalent JSON Representation:
{
"fruits": [
{
"name": "mango",
"physical": {
"color": "yellow",
"shape": "oval"
},
"varieties": [
{ "name": "alphonso" },
{ "name": "dasheri" }
]
},
{
"name": "banana",
"varieties": [
{ "name": "robusta" }
]
}
]
}
In JSON, you can see that the fruits
array now contains objects, each of which can have other nested objects (like physical
) and arrays (like varieties
).
Common Mistakes and How to Avoid Them
While TOML is simple, there are some strict rules about how arrays and tables are defined. Here are a few common mistakes that TOML will throw errors for:
Parent Missing or Misplaced Order
# INVALID TOML DOC
[fruit.physical] # subtable, but to which parent element should it belong?
color = "red"
shape = "round"
[[fruit]] # parser must throw an error upon discovering that "fruit" is
# an array rather than a table
name = "apple"
Now, you may be wondering: Why is this invalid?”
- The subtable
[fruit.physical]
doesn’t know where it belongs because the parentfruit
table hasn’t been defined yet. - The
[[fruit]]
entry is incorrect because it tries to define a table inside an array, but thefruit
array was not set up beforehand.
In short, in TOML, a table (like
fruit
) must be defined before any subtables or arrays of tables inside it. The parser ensures that all parent elements exist before any child elements are defined.
Appending to an Already Defined Array
Basically, attempting to append to a statically defined array, even if the array is empty, should result in a parse-time error.
# INVALID TOML DOC
fruits = []
[[fruits]] # Not allowed
Here, you’re trying to append a new table [[fruits]]
to an already defined empty array (fruits = []
). Since fruits
is statically defined as an array, trying to add a table to it will result in an error.
Arrays and tables need to be defined in the correct order. Once an array is defined, you can’t mix the structure with normal tables or attempt to append a table to it after defining it.
Conflicting Array and Table Names
A conflict happens when you try to define a normal table using the same name as an existing array, which leads to a parse-time error. Similarly, if you try to turn a normal table into an array, it will also cause a parse-time error.
# INVALID TOML DOC
[[fruits]]
name = "mango"
[[fruits.varieties]]
name = "alphonso"
# INVALID: This table conflicts with the previous array of tables
[fruits.varieties]
name = "dasheri"
[fruits.physical]
color = "yellow"
shape = "oval"
# INVALID: This array of tables conflicts with the previous table
[[fruits.physical]]
color = "green"
Here,
- First part (
[[fruits]]
and[[fruits.varieties]]
): You define an array of tables with[[fruits]]
and a nested array[[fruits.varieties]]
for the different fruit varieties. - Error 1: The line
[fruits.varieties]
tries to redefinefruits.varieties
as a regular table. This is a conflict becausefruits.varieties
was already defined as an array of tables ([[fruits.varieties]]
). You can’t redefine it as a table. - Error 2: Similarly, the line
[[fruits.physical]]
tries to definefruits.physical
as an array of tables, but it was already defined as a regular table ([fruits.physical]
). This causes a conflict.
In TOML, if a structure is defined as an array or a table, you cannot redefine it as the opposite later.
So,
Rules for Arrays of Tables in TOML:
- Appending to arrays: You can’t append to a statically defined array (
[]
). The array must be explicitly defined before you add any tables to it. - Conflicts between arrays and tables: You cannot define a table with the same name as an array, nor can you redefine a table as an array, or vice versa.
Inline Tables in an Array (Compact Format)
As we know, inline tables are defined using curly braces {}
. However, we can also represent an array of inline tables (a list of objects), which is also valid in TOML. Here is a valid TOML format:
# Using Inline Tables
points = [
{ x = 1, y = 2, z = 3 },
{ x = 7, y = 8, z = 9 },
{ x = 2, y = 4, z = 8 }
]
Here,
points
is an array (list).- Each
{ x = ..., y = ..., z = ... }
is an inline table (a dictionary-like structure inside a list). - This is ideal when you have small data structures.
Now, how do we represent this using TOML’s standard array of tables format?
Alternatively, we can use TOML’s standard table array syntax ([[points]]
for multiple objects):
# Using Standard Table Array Syntax
[[points]]
x = 1
y = 2
z = 3
[[points]]
x = 7
y = 8
z = 9
[[points]]
x = 2
y = 4
z = 8
Here,
[[points]]
defines an array of tables, meaning multiple objects under points
.
Each [[points]]
entry creates a new table (object) with x
, y
, and z
values.
This format is easier to read when you have a large number of objects.
Both approaches are valid. The inline table format is more compact, while the table array format is more readable for larger datasets.
Conclusion
TOML is a simple yet powerful format for managing configuration files. To use it effectively, it’s essential to understand structures like arrays of tables, subtables, and inline tables. Since TOML enforces strict syntax rules, especially for arrays and tables, paying close attention to these details will help avoid errors. Once familiar with its structure, you can create well-organized, human-readable configuration files that are easy to maintain and modify.