TOML: The Config Format Rust Developers Love

TOML: The Config Format Rust Developers Love

If you've ever opened a Cargo.toml or a pyproject.toml, you've already read TOML. It's the config format that Rust chose for its entire package ecosystem, and it's been quietly spreading to Hugo, Gitea, Forgejo, Taplo, and dozens of other tools. There's a reason developers keep reaching for it instead of YAML or INI.

Where TOML Came From

TOML stands for Tom's Obvious Minimal Language. Tom Preston-Werner — co-founder of GitHub and creator of Semantic Versioning — published the spec in 2013. He was frustrated with existing config formats: INI was too loose and inconsistent, XML was verbose and painful, and YAML had too many footguns hiding in its whitespace-sensitive syntax.

His goal was a format that maps unambiguously to a hash table. Read it once, parse it once, no surprises. The current stable version is TOML 1.0.0, finalized in January 2021 after years of community refinement.

TOML Syntax in Practice

The basic building block is a simple key-value pair with an explicit equals sign:

name = "utilitykit"
version = "1.4.2"
debug = false
port = 3001

Every value has a type. Strings use double quotes. Booleans are lowercase true or false. Integers and floats are unquoted. This matters — we'll come back to it.

Tables (Objects)

A table is declared with square brackets and acts like an object or dictionary:

[server]
host = "0.0.0.0"
port = 8080
timeout = 30

[database]
url = "postgres://localhost/myapp"
pool_size = 5

This is roughly equivalent to the JSON:

{
  "server": { "host": "0.0.0.0", "port": 8080, "timeout": 30 },
  "database": { "url": "postgres://localhost/myapp", "pool_size": 5 }
}

You can also write inline tables on a single line, which is useful for compact data:

point = { x = 1, y = 2 }

Array of Tables

This is where TOML earns its keep. Use double brackets [[...]] to define an array of objects:

[[dependency]]
name = "express"
version = "4.18"

[[dependency]]
name = "sharp"
version = "0.33"

Each [[dependency]] block appends a new object to the dependency array. It reads linearly from top to bottom, which makes diffs much cleaner than their JSON or YAML equivalents.

Types and Literals

TOML's type system is explicit and complete. The supported types are:

  • String"double quoted", 'literal (no escapes)', """multi-line basic""", or '''multi-line literal'''
  • Integer42, -7, 1_000_000 (underscores allowed for readability)
  • Float3.14, 6.02e23, inf, nan
  • Booleantrue, false
  • Datetime2024-01-15T10:30:00Z (RFC 3339, full datetime or date/time only)
  • Array[1, 2, 3] or multi-line
  • Table{ key = "value" } or header syntax

The datetime type is a major practical win. In JSON or YAML, dates are just strings — your application code has to decide whether "2024-01-15" is a date or an arbitrary string, and implicit type coercion can burn you. TOML bakes dates into the spec.

How TOML Compares to YAML

YAML is technically a superset of JSON and has a much larger type system, but that breadth creates real hazards. The classic example:

# YAML: this is the boolean false, not the string "false"
debug: false

# These are also booleans in YAML 1.1:
verbose: yes
enabled: on

YAML 1.1 treated yes, no, on, off, true, and false as booleans. This caused a notorious problem with country codes — NO for Norway would silently become false. YAML 1.2 fixed most of this, but the behavior depended on which parser you were using.

TOML has no implicit type coercion. "yes" is always a string. false is always a boolean. No ambiguity.

YAML also uses significant whitespace for structure, so an indentation mistake silently changes meaning. TOML uses explicit delimiters — structure is harder to accidentally break.

Where YAML still wins: deeply nested or complex documents. TOML tables get verbose at three or four levels of nesting, while YAML's indentation keeps things compact. For small configs, TOML is usually the easier read. For complex document trees, YAML may still be the better fit.

How TOML Compares to INI

INI files predate modern configuration management by decades. The format is widely understood, but it has no standard — every program implements a slightly different dialect. No standard types, no arrays, no nested structure (or a non-standard [section.subsection] hack), and no boolean representation that every parser agrees on.

TOML was designed to look like a well-specified INI format, which is why Rust's Cargo.toml looks familiar to anyone who's read a .ini file. But TOML has a real spec, a real type system, and real nesting.

You can read more about the INI format and how it compares in our INI files overview.

Where TOML Is Actually Used

The highest-profile user is Rust. Every Rust project has a Cargo.toml that declares package metadata, dependencies, build targets, and workspace configuration. The community has normalized TOML as the standard for all tooling in that ecosystem.

Python adopted TOML for pyproject.toml via PEP 518 and PEP 621. This is now the preferred way to specify build systems, dependencies, and tool configuration (replacing the older setup.cfg and setup.py approach).

Hugo — the static site generator — uses TOML for site configuration. So does Gitea. The Go ecosystem more broadly has adopted TOML for many CLI tools.

Taplo is a dedicated TOML language server and formatter, which shows the format has matured enough to have its own IDE tooling ecosystem.

When to Reach for TOML

TOML is a good choice when:

  • You're building a CLI tool and want a config file format users can read and edit manually
  • Your config has typed values (ports, booleans, dates) and you want the type to be unambiguous
  • You want clean diffs on arrays of structured items (use [[array-of-tables]])
  • Your team works in the Rust or Python ecosystems where TOML is already the convention

It's a worse choice when:

  • You need deeply nested structures (YAML handles that more compactly)
  • You need to serialize arbitrary data from an API (JSON is the universal choice there)
  • Your environment only has parsers for JSON or YAML

Validating and Formatting TOML

TOML documents are fairly easy to validate manually because the syntax is explicit, but you can also paste any TOML into a JSON conversion tool to check the parsed structure. Our JSON Formatter can help you inspect the resulting object after conversion, and JSON to YAML is useful if you need to work with both formats simultaneously on a project.

TOML isn't the most popular config format on the internet, but it's probably the most thoughtfully designed. If you're starting a new project and you have a choice, it's worth serious consideration.

FAQ

Should I use TOML or YAML for new projects in 2026?

For configs with shallow nesting and explicit types (CLI tools, Rust/Python projects), TOML wins on safety and readability. For deeply nested structures (Kubernetes manifests, GitHub Actions, Ansible playbooks), YAML's compact indentation wins despite the footguns. Pick based on the depth of your data: 1-2 levels of nesting → TOML; 4+ levels → YAML. JSON is the right answer when neither human-friendliness nor comments matter.

Why did Rust pick TOML over JSON or YAML?

Three reasons: explicit types (no ambiguity between "true" the string and true the boolean), comment support (JSON has none, YAML has many), and clean diffs on dependency arrays via [[array-of-tables]] syntax. Cargo manifests evolve constantly across releases, and the TOML format keeps PR diffs readable in ways YAML's whitespace-sensitive structure doesn't.

Does TOML support deeply nested structures?

Yes, but it gets verbose. Nested tables use dot notation: [server.tls.cert] is three levels deep. Beyond 3-4 levels, TOML's table headers start to feel cluttered. If your config naturally has 5+ levels of nesting (rare), YAML or JSON might fit better. Most application configs sit comfortably at 2-3 levels, where TOML shines.

Can I store secrets in TOML config files?

Technically yes, but you shouldn't commit secrets to any config format — TOML, YAML, JSON, or otherwise. Use environment variables, a secret manager (Vault, AWS Secrets Manager, 1Password), or a separate .env file that's gitignored. TOML configs typically reference secrets indirectly: database_url_env = "DATABASE_URL" and the application reads the actual value from process.env.DATABASE_URL.

How do I convert TOML to JSON or vice versa?

In Python: import tomllib; data = tomllib.loads(toml_string); json.dumps(data) (Python 3.11+). In Rust: toml crate plus serde_json. In Node: @iarna/toml or toml-j0.4. The conversion is mostly lossless — TOML and JSON map to the same data shape — but TOML's datetime type becomes a string in JSON, and JSON's null has no TOML equivalent. For one-off conversions, online tools work fine.

Why doesn't TOML support null or undefined?

By design — Tom Preston-Werner explicitly excluded null because it's a frequent source of bugs and ambiguity. To indicate "no value," omit the key entirely. To indicate "no value, but I want to make it explicit," some teams use empty strings or convention-based sentinels ("", 0, false). The lack of null forces clearer config schemas: either a value exists or it doesn't.

Can TOML handle environment-specific overrides like Hugo's `config.toml` vs `config.production.toml`?

TOML itself doesn't have a layering or include mechanism — that's the application's job. Hugo merges multiple TOML files into a single config object. Cargo has workspaces with inherited configuration. Your application can layer TOML files (default → environment → user) using the standard library's TOML parser. The format is just data; the merging logic lives in your tool.

Is TOML's array-of-tables syntax really better than YAML's array-of-maps?

For structured arrays of objects (like dependencies, plugins, user lists), yes — diffs are cleaner because adding a new entry adds a contiguous block of lines rather than restructuring the whole array. Compare adding a dependency to Cargo.toml (one new [[dependency]] block) vs YAML (an indented map inside an array). The structural clarity is why Rust's ecosystem standardized on it.