TOML Formatter
Canonical formatting for TOML. Paste your document and receive a normalized version with consistent spacing, string quoting, and table layout. Everything runs in your browser.
What this tool does
It parses a TOML document, builds the value tree in memory, and re-emits it as canonical TOML — with consistent spacing around =, normalized string quoting, deterministic table layout, and an optional sort order. Parsing follows the TOML 1.0 spec (dotted keys, every date variant, hex/octal/binary numbers); emission honours the option set on the widget.
The intent it closes: "I want this TOML file to look the way the project would have produced it if everyone had been disciplined." Useful when a config has been touched by three tools and four people, when a generator dumps unformatted output, or when you want a diff that only shows real changes rather than whitespace churn.
Comments are kept. They used to be dropped — a parser produces a value tree, and a value tree has nowhere to put them — so this tool now reads the source a second time, on its own, recording only which key or table header each comment was written against. The emitter looks each one up as it writes that line out, which means a comment follows its key even when Sort moves it. The status line reports how many were found and how many landed.
Two cases still lose one: a comment on a key inside a table you have asked to be written inline has no line of its own to sit on, and a comment attached to something the emitter does not write at all goes with it. Both are counted, and the status line says so rather than staying quiet.
When you'd reach for it
- Normalize a hand-edited
pyproject.tomlorCargo.toml. Get spacing, quoting, and ordering back to the form your team review expects. - Reduce diff noise before committing. Format both sides of a refactor so the only changes left are real ones.
- Validate while you format. If parsing fails, the status bar says so — a malformed config never produces output, so a clean format is also a syntax-valid file.
- Alphabetize for searchability. Pick the Alphabetical sort to make a giant table scannable.
- Inline small tables. Switch Tables to Auto with a small Inline ≤ threshold to keep two-key tables compact while letting bigger ones expand.
- Match a project style guide. Most rust-ecosystem projects use basic quotes, no trailing commas, spaced arrays — those are the defaults here.
How formatting works
One click of Format runs three stages.
1. Parse the input
The full TOML 1.0 grammar is parsed into a JavaScript value tree. Dotted keys collapse into nested tables, dates become real Date-shaped values, integers preserve their hex/octal/binary intent. If any of this fails, the status bar shows the parser error and no output is written — formatting and validation share the same parser.
2. Order keys
Each table's keys are walked in one of three orders. Preserve order writes them in input order. Alphabetical sorts case-sensitively. Alpha, tables last sorts case-sensitively, but pushes any key whose value is a table (or array of tables) to the end — useful for keeping scalar config at the top of every section.
3. Emit, deciding inline vs block per table
For each table, the formatter decides whether to write it inline ({ a = 1, b = 2 }) or as a section ([name]). Always inline and Always block are the explicit choices; Auto uses the Inline ≤ threshold and only inlines leaf tables (no nested tables of their own). Arrays-of-tables ([[…]]) are always block. Section blocks get a blank line between them.
Options reference
Sort
![]()
Preserve order keeps the order keys appeared in your input. Alphabetical sorts keys in each table A-Z. Alpha, tables last is the same but moves table-valued keys after scalars — handy when you want name and version before [dependencies] in every section.
Tables (inline mode)
![]()
Auto consults the Inline ≤ threshold; tables small enough and shallow enough are emitted inline. Always block writes every table as [section] headers — verbose but uniform. Always inline writes everything as { … }; mostly useful for snippets, not full config files.
Inline ≤
![]()
The maximum number of keys a leaf table can have to be emitted inline under Auto. 0 means "never inline anything from Auto mode" — effectively a softer Always block. Try 3 for a balance.
Arrays
![]()
Spaced writes [1, 2, 3] with a space after each comma. Compact writes [1,2,3]. Rust and Python ecosystems generally use Spaced; some Go and embedded configs use Compact.
Quotes
![]()
Basic uses standard double quotes ("hello") and applies escapes when needed. Literal uses single quotes ('hello') — no escapes are interpreted, so backslashes stay literal. Auto picks per-string: literal quotes for strings without single quotes and with characters that would need escaping in basic mode (e.g. Windows paths), basic otherwise.
Trailing ,
![]()
Off writes [1, 2]. On writes [1, 2,]. TOML 1.0 allows both; trailing commas reduce diff churn when arrays grow.
Wrap @
![]()
Soft wrap column for inline arrays. 0 means "no wrap — arrays stay on one line regardless of width". Set to 80 to break long arrays onto multiple lines once they pass column 80.
Output format
- Encoding: UTF-8, no BOM.
- Line endings: LF (
\n). A trailing newline is appended when the document is non-empty. - Spacing: exactly one space on each side of
=. Section headers get a blank line before them (except the first). - Comments: kept by default, re-attached above the key or header they were written against, with an end-of-line comment restored two spaces after the value. Set Comments to Drop for the old behaviour.
- Whitespace inside arrays: driven by the Arrays option, plus Wrap @ for long inline arrays.
- Download:
formatted.tomlwithtext/plaincontent type.
Example
Input:
title="Example"
[owner]
name="Tom"
[database]
enabled=true
ports=[8001,8001,8002]
connection_max=5000
Output (defaults — Preserve, Auto, Basic quotes, Spaced arrays, no trailing comma):
title = "Example"
[owner]
name = "Tom"
[database]
enabled = true
ports = [8001, 8001, 8002]
connection_max = 5000
Note the blank line before each section, the space on both sides of =, and the spaced commas inside the array. The duplicate 8001 survives — formatting doesn't deduplicate values.
Recipes by intent
Clean up pyproject.toml for a PR
Sort Preserve order, Tables Always block, Quotes Basic, Arrays Spaced, Trailing , off. Matches Poetry's expected style.
Alphabetize a giant config you have to search by eye
Sort Alpha, tables last. Scalars (name, version, edition) cluster at the top of every section; nested tables drop to the bottom. Makes Cargo.toml with many features readable.
Compact a config for an embedded device or env var
Tables Always inline, Arrays Compact, Inline ≤ 64. Produces the smallest valid TOML, useful when serializing config into a single env var or QR code.
Wrap long arrays automatically
Wrap @ 80. Inline arrays past column 80 break across lines, keeping diffs reviewable when an array grows. Below the threshold they stay on one line.
Diff-friendly canonical form
Sort Alphabetical, Trailing , on, Quotes Basic. With everyone formatting before commit, diffs only show semantic changes — no whitespace, no key reordering.
Limits and performance
- Memory-bound. The parser builds the full value tree before emission, so input, tree, and output all sit in memory at once.
Cargo.lock-sized files (thousands of lines) are instant; multi-megabyte TOML can take a second or two. - Comments survive a re-layout, not a re-shaping. They are matched to a key path, so sorting and re-indenting keep them. Inlining a table removes the lines its comments were attached to, and those are reported as not kept rather than silently lost.
- Key order survives only in Preserve mode. Alphabetical orderings rewrite the layout completely; that's the trade-off.
- The textarea is the slow bit on huge files. Around 50 MB+ of output, rendering it back into the right-hand pane can lag even after the format completes. Use Download .toml instead of Copy.
Errors and how to fix them
"Parse error" / status bar goes red on click
The input isn't valid TOML 1.0. The most common causes: unclosed quotes, an array that opens with [ but never closes, a missing = between key and value, or a duplicate key inside the same table. Try the TOML Validator — it points at the exact line.
Output is empty
The root of a TOML document must be a table. If your input is empty, or you passed in a TOML fragment that wasn't a top-level table (just a value), the formatter refuses to emit anything.
My # comments disappeared
Check the Comments option is on Keep, then read the status line — it says how many comments were found and how many were written back. A gap between the two means the missing ones were attached to keys that no longer have a line of their own: usually a table pushed inline by Tables or Inline ≤. Turn inlining off and they come back.
Key order changed unexpectedly
Either Sort isn't on Preserve order, or your input had the same dotted key written in two different forms (a.b = 1 and [a]\nb = 1) — both collapse to the same tree, and the emitter picks one. Pick Preserve order and consolidate the duplicates in your source.
String quoting style flipped
The Quotes option canonicalizes every string. If your input had mixed quoting, the output normalizes it. Switch to Auto if you want the formatter to pick per-string instead of forcing one style everywhere.
FAQ
Will formatting change the meaning of my config?
No. The tree is semantically identical to your input — only whitespace, quote style, key order (when sorted), and inline-vs-block decisions change. Anything that parses to the same value tree round-trips to the same effective config.
Does it handle the full TOML 1.0 spec?
Yes, including dotted keys, every date variant, hex/octal/binary integers, and array-of-table syntax ([[…]]). Anything the spec disallows (mixed array types, duplicate keys) errors out at parse time.
Is my data uploaded?
Never. The parser and emitter run entirely in your browser. You can disconnect from the network before clicking format and it still works — safe for files with secrets or API keys.
Why is there a blank line between sections?
Because that's the dominant convention across Rust, Python, and Go ecosystems, and because a blank line makes section boundaries scannable. It's emitted automatically and isn't configurable on this widget.
How are comments preserved without a CST?
With a second pass rather than a bigger parser. The value tree comes from the TOML parser as before; alongside it a small scanner walks the same source text and builds one map from key path to the comments written against it — the block above a line, and the trailing one after the value. It has to understand quoting, multi-line strings and wrapped arrays well enough not to mistake a # inside a string for a comment, which is most of its code. The emitter then asks that map for a path as it writes it. The two agree because they build the same path string, and neither needs to know about the other.
The limitation of the approach is honest and visible: a comment can only be written back if the key it belongs to still gets a line. Nothing is guessed at, and nothing is moved to a place it was not.