← Back to Blog Formats

TOML Format Explained: Syntax, Types & Common Config Pitfalls (2026)

Got a TOML file that won't parse?

Paste it into the TOML Formatter & Validator — it flags the exact line of any syntax error, reformats consistently, and runs entirely in your browser.

TOML powers some of the most consequential files in modern development: Cargo.toml defines every Rust build, pyproject.toml defines Python packaging, taplo configs shape editor tooling, and countless apps chose it as their config format after evaluating JSON and YAML. This guide covers the complete language — it is small enough to learn in one sitting, which is precisely the point.

1. What Is TOML and Why It Exists

TOML means Tom's Obvious, Minimal Language, created by Tom Preston-Werner (GitHub co-founder) in 2013. The requirements were: obvious to a human reading it cold, minimal enough that the full spec fits in one screen, unambiguous when parsers map it to a data structure, and expressive enough for configuration (not general data interchange — that remains JSON's job).

The design landed between two poles. JSON had no comments and no typed scalars (everything is a string, number, bool, or container — no dates). YAML had comments but notorious edge cases (Norway's country code parsing as boolean NO, sexagesimal surprises, indentation anxiety). TOML keeps comments and typed values while forbidding indentation semantics entirely — structure comes only from explicit table headers.

2. Core Syntax: Keys, Values, Comments

A TOML document is a flat sequence of key-value pairs; grouping comes from tables (next section). Every line is one assignment:

# Comment: anything after # is ignored
title = "My Project"
port = 8080
debug = true
ratio = 0.75
max_retries = 1_000_000      # underscores are digit separators
pi = 3.14159

"quoted key" = "keys can be quoted when they contain spaces"
bare_key-2 = "or start with letters, digits, _ and -"
  • Keys are bare (letters, digits, _, -), quoted, or dotted (server.http.port — sugar for nested tables).
  • Values are typed: "strings", integers, floats, true/false, [arrays], { inline = "tables" }, and datetimes.
  • Whitespace around = is ignored; the newline ends the assignment.

3. Tables and Arrays of Tables

Bracketed headers group keys until the next header:

[server]
host = "example.com"
port = 443

[server.tls]
enabled = true
min_version = "1.2"

[limits]
max_connections = 512
timeout_seconds = 30

The critical rule: a table header can only be written once. Two [server] headers means a duplicate-key error — append keys to the existing table instead. Dotted keys and headers are equivalent sugar: [server.tls] is the same as nesting [server] then [tls].

For lists of records, use arrays of tables — each [[name]] starts a new element:

[[workers]]
name = "ingest"
threads = 4

[[workers]]
name = "render"
threads = 16

This maps directly to a list of objects — the standard way to express repeated entities (workers, servers, migrations) in config files.

4. Every Type, With Examples

TypeExamplesNotes
String"basic", 'literal', """multi"""Basic supports escapes; literal forbids them
Integer42, -7, 0xFF, 0o755, 0b1010Underscore separators allowed
Float3.14, -0.01, 5e+22, 1e6, inf, nanMust have digits around the decimal point
Booleantrue, falseLowercase only — True is a parse error
Datetime1979-05-27T07:32:00Z, 1979-05-27, 07:32:00Offset, local datetime, local date, local time
Array[1, 2, 3], mixed types allowedTrailing commas are legal
Inline tablepoint = { x = 1, y = 2 }Single-line by design; no newlines inside
# All four datetime flavors
released = 1979-05-27T07:32:00-08:00   # offset date-time
midnight = 1979-05-27T00:32:00         # local date-time
birthday = 1979-05-27                  # local date
alarm    = 07:32:00                    # local time

5. Strings and Multi-line Text

Four string flavors exist, and choosing the wrong one is the most common formatting headache:

basic    = "Escapes: \n \t \" \\ \u00E9"
literal  = 'No escapes here: C:\Users\name stays literal'
multi    = """
Line 1
Line 2   """           # closing delimiter's indentation strips all lines
multi_lit = '''
C:\path\to\file
remains exactly as typed
'''
  • Basic supports escape sequences; literal (') treats every character verbatim — ideal for Windows paths and regexes.
  • Multi-line (""" / ''') allows newlines; a newline immediately after the opening delimiter is trimmed, and the closing delimiter's indentation is stripped from every line — keep closing """ flush-left or be deliberate about indentation.

6. The Six Pitfalls That Break Configs

  1. Defining a table twice. [server] appearing again later is an error, not a merge. Same for duplicate keys — the spec forbids it outright.
  2. Tabs for indentation. Plain key-value lines need no indentation, but multi-line strings and arrays allow whitespace — and only spaces. Tabs are a parse error.
  3. Unquoted "strings." version = 1.2 parses as float 1.2, not the string "1.2"; version = 1.20 becomes 1.2 — data corruption without any error. Quote version numbers, ports-as-strings, anything that must preserve its form.
  4. Capitalized booleans. True/FALSE are not TOML. The spec is lowercase-only.
  5. Arrays split across lines with sloppy commas. Newlines inside arrays are fine, but there must be a comma after each element except the last — before the newline, not after the closing bracket.
  6. Inline tables used as partial definitions. An inline table is complete and atomic: you cannot later add keys to point from a separate line. Use a [table] header instead.

Every one of these produces a parse error with a line number — or worse, in case 3, silent data change. Running the file through a validator before committing is the cheap insurance policy: paste into the TOML to JSON Converter and it reports line N: duplicate key or line N: expected value instead of a failing CI job.

7. TOML vs YAML vs JSON vs INI

PropertyTOMLYAMLJSONINI
CommentsYes (#)Yes (#)No (standard)Yes
Typed scalarsYes + datetimesYesYes (no dates)No — all strings
Whitespace-sensitiveNoYes — indentation defines structureNoNo
Type surprisesMinimalNotorious (NO, 0123)NoneNone
Nesting depthTables, arbitraryArbitraryArbitrarySections only, flat
Machine interchangeRareCommon (k8s)The standardNo
Spec size~60 linesLargeMediumDe facto, fragmented

Decision rule: API payloads → JSON; configuration files → TOML; infrastructure manifests where YAML is entrenched (Kubernetes) → YAML, but validate it. The JSON-vs-XML half of this decision is covered in depth in XML vs JSON.

One workflow note: TOML and YAML/JSON often coexist — tools that accept TOML configs frequently consume JSON or YAML data alongside. When your config points at data files, the YAML Validator and JSON Formatter cover the other two legs of that triangle.

8. Annotated Real-World Files: Cargo.toml and pyproject.toml

The syntax becomes concrete in the two files most developers meet first. A trimmed but realistic Cargo.toml:

[package]
name = "telemetry-agent"
version = "0.4.2"          # always a string � 0.42 would become the float 0.42
edition = "2021"
rust-version = "1.74"
description = "Metrics shipping agent"
license = "MIT OR Apache-2.0"

[dependencies]
serde = { version = "1", features = ["derive"] }   # inline table: complete, single-line
tokio = { version = "1.36", features = ["rt-multi-thread", "macros"] }
tracing = "0.1"

[dev-dependencies]
criterion = "0.5"

[profile.release]
lto = "fat"
codegen-units = 1
strip = true

[[bin]]
name = "telemetry-agent"
path = "src/main.rs"

Notice the patterns that recur in real manifests: version numbers are quoted strings (an unquoted version = 1.36 would be a float, and version = 1.10 would silently become 1.1 � the corruption trap from section 6), feature lists use inline tables or string arrays, and the array-of-tables syntax ([[bin]]) describes multiple binaries without inventing a new structure.

A representative pyproject.toml for a modern package:

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "orders-etl"
version = "2.1.0"
description = "Nightly order pipeline"
readme = "README.md"
requires-python = ">=3.11"
license = { text = "Proprietary" }
dependencies = [
  "httpx>=0.27",
  "pydantic>=2.6",
  "structlog>=24.1",
]

[project.optional-dependencies]
dev = ["pytest>=8.0", "ruff>=0.3", "mypy>=1.9"]

[project.scripts]
orders-etl = "orders_etl.cli:main"

[tool.ruff]
line-length = 100
target-version = "py311"

[tool.ruff.lint]
select = ["E", "F", "I", "UP"]

[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-q --strict-markers"

Read the conventions off this file, not just the syntax: dependency arrays go multi-line with trailing commas (legal since arrays may span lines and allow trailing commas); tool-specific settings nest under [tool.<name>] so one file configures several tools without collisions; and anything a human reads aloud � versions, Python requirements � stays quoted so it can never drift into numeric parsing.

9. Spec Sharp Edges: Define-Before-Use, Key Identity, and Mixed Arrays

Beyond the six pitfalls, three rules decide whether technically plausible documents parse. Each one produces a confusing error the first time you hit it.

Define-before-use for dotted keys

# Valid: the dotted keys themselves create the parent tables
fruit.apple.color = "red"
fruit.apple.taste = "sweet"

# Invalid: [fruit] conflicts � fruit was already created
# implicitly by the dotted keys above
[fruit]
variety = "malus"

A dotted key defines the tables it passes through, exactly the way a header would; those implicit tables cannot be reopened later. The corollary: define before you extend, never after. The same logic applies in reverse � after writing a full [fruit.apple] header, you cannot later add keys via fruit.apple.color = "green" at top level.

What counts as the same key

  • Bare keys compare case-sensitively: name and Name are different keys. (A few tools fold case on top; the spec does not.)
  • Quoted and bare forms of the same text collide: "port" and port are the same key, so quoting does not sneak a duplicate past a validator.
  • Dotted paths compare segment by segment: a.b.c and "a.b".c address the same table, because quoted segments decode to the same text before comparison.

Mixed-type arrays and heterogeneous data

TOML arrays may hold any types in any order � [1, "two", true, 1979-05-27] parses cleanly. That freedom is convenient for argument lists and dangerous for schemas: nothing stops ports = [80, "443", 8080], and consumers will disagree about what to do with it. If another tool reads the value, constrain types in review or validate against a schema; the grammar deliberately does not enforce homogeneity for you.

10. When TOML Is the Wrong Tool

TOML wins for configuration and loses for data. The failure modes are predictable:

WorkloadWhy TOML strugglesUse instead
Deeply nested state or documentsEvery level needs a header or dotted key; inline tables cannot span lines in 1.0, so wide nested structures get unreadable fastJSON, YAML
Matrices and lists of listsArrays of arrays parse, but no header syntax covers them and editing nested inline tables is error-proneJSON, YAML
Machine-generated large payloadsMinimal streaming writer support; most emitters reformat the whole documentJSON or JSONL
Configs that need includes or overlaysNo include, import, or anchor mechanism � composition is left to the applicationYAML anchors, or app-level overlay logic
Strict schema interchange between servicesThe JSON Schema ecosystem and HTTP tooling assume JSONJSON

The absence of includes deserves emphasis because it is the most common surprise. TOML cannot say "load base.toml, then apply prod.toml on top" � the application implements that itself, or refuses to. If overlay or environment-layered configuration is a hard requirement, design the layering in the application or pick a format with native composition. In the Docker Compose world, YAML anchors and merge keys fill exactly that gap, as the Docker Compose env-file vs environment comparison shows.

11. TOML 1.0 vs 1.1, and the Tooling That Keeps You Honest

TOML 1.0.0 (2021) is what Cargo.toml, pyproject.toml, and virtually every parser implement today. The 1.1 line of the spec is still a draft; its headline proposals, all backward-compatible with valid 1.0 documents:

Proposed in 1.1EffectPractical impact
Newlines allowed inside inline tables{ ... } blocks can wrap across linesLong feature lists stop needing full table headers
Trailing commas in inline tablesMulti-line inline structures stay diff-friendlyCleaner version-control diffs
Unicode bare keysUnquoted keys may use non-ASCII lettersMainly relevant for localized configuration
\e escape plus broader escaped control charactersCleaner escape sequences in stringsTerminal color config without \u001B-style noise
Seconds optional in times07:32 accepted alongside 07:32:00Cosmetic parity with human-written times

Until your parser explicitly documents 1.1 support, write 1.0: a file relying on newlines inside inline tables parses in one tool and fails in the next, which is precisely the ambiguity TOML set out to eliminate. Pin CI validators to 1.0 semantics and treat any 1.1-only syntax as an experiment.

Formatting and editor tooling

The community converged on a small, stable set of tools:

  • Taplo � the de facto formatter and language server. taplo format applies a canonical style (alignment off, optional key ordering); taplo check fails CI on drift. Editors running the Taplo language server get live diagnostics and format-on-save, which eliminates the duplicate-table-header class of errors before commit.
  • Editor built-ins � VS Code, JetBrains IDEs, Neovim, and Zed ship TOML highlighting, and most of them pick up the Taplo server or an equivalent with diagnostics.
  • Pre-commit and CI hooks � run taplo check alongside your linter so manifest formatting is treated like code style and diffs stay reviewable.
  • Converters and validators � round-trip through the TOML Formatter & Validator whenever a parse error appears; it reports the failing line instead of the generic "invalid config" message most build tools print.

Formatting conventions the community settled on, with or without a formatter: one key per line; sort dependency tables alphabetically (manually or via taplo's key-ordering option); trailing commas in multi-line arrays; and prefer a full table header over long dotted keys once a dotted path exceeds two segments � reviewers navigate [tool.mypy] far more easily than twelve scattered mypy.* lines. For teams migrating from Java-style .properties or INI files, map section headers to [tables], quote every value (INI has no types, so timeout=30 was a string � decide deliberately whether it becomes "30" or 30), join multi-value keys into arrays, and expect duplicate keys to become errors: the permissive last-one-wins habit of properties files is the migration bug that bites most often.

Conclusion

TOML earns its adoption by refusing to be clever: no indentation rules, no implicit typing surprises, one header per table, and values that look like what they are. Learn the four string types, remember that version = 1.20 is a float, and never write [table] twice — and the format will never surprise you. That predictability is exactly why build tools trust it with their most important files.

Frequently Asked Questions

What is TOML?
Tom's Obvious, Minimal Language — a config format designed to be easy to read and write, unambiguous when mapped to a data structure, and expressive enough for real configuration. It uses key-value pairs grouped into tables.
What does TOML stand for?
Tom's Obvious, Minimal Language, named after creator Tom Preston-Werner, co-founder of GitHub. The name signals the goal: an obviously readable config format with a minimal spec.
Why do Rust, Python, and Cargo use TOML?
pyproject.toml and Cargo.toml chose TOML because it has typed values (integers, floats, booleans, dates), a clear spec unlike INI, and no indentation rules or type coercion surprises unlike YAML.
TOML vs YAML vs JSON — which should I use?
JSON for machine-to-machine APIs. YAML for config with comments plus rich types, accepting its edge cases. TOML for configuration files: comments, unambiguous types, no whitespace sensitivity, tiny rule set.
How do you write arrays and tables?
Arrays: ports = [80, 443, 8080]. Tables: [server] headers with keys below. Arrays of tables: repeat [[workers]] for each entry, mapping to a list of objects.
What data types does TOML support?
Basic, literal, and multi-line strings (both quote styles); integers with underscore separators and hex/octal/binary forms; floats; booleans; four datetime flavors; arrays; inline tables.
Why is my TOML file failing to parse?
Usual causes: tabs where spaces are required, redefining a table or key, unquoted values where strings are expected, and array comma placement. A validator reports the exact failing line.
Can TOML have comments?
Yes — anything after # is a comment, on its own line or trailing a value. This is one reason config formats like TOML and YAML are preferred over standard JSON, which has no comments.