T tomlkit·org
Inspect Formatter Validator Convert TOMLJSON JSONTOML TOMLYAML YAMLTOML INITOML TOMLINI .envTOML TOML.env TOMLTS Transform Sort keys Flatten Minify Compare Diff Merge

TOML Diff

updated 8 June 2026

Compare two TOML documents semantically — by tree, not by text. Whitespace, comments, and key order are ignored, so you see only the changes that actually affect the parsed config.

semantic · key-order-independent · whitespace-blind

What this tool does

It parses both TOML documents — A on the left, B in the middle — into value trees, then walks them together by leaf path and reports every place the two trees differ. Because it compares parsed values and not text, whitespace, key order, quote style, and table layout are all irrelevant: it's a semantic diff. The result, in the right pane, marks each difference with + (a key in B but not A), - (a key in A but not B), or ~ (a key present in both whose value changed), under a header line that tallies the counts.

The intent it closes: "I have two versions of a config and I want to see what actually changed, not what the formatter rearranged." That's the review you do on a pull request that touches Cargo.toml or pyproject.toml, the sanity check after a generator or a merge tool rewrote a file, or the before/after comparison when you're not sure two configs are really equivalent.

It owns one job: comparing two TOML documents semantically. It does not merge them, sort them, or convert them — the related tools below do that. If a document fails to parse, there's no tree to compare, so a clean parse on both sides is a prerequisite.

When you'd reach for it

  • Review a config change without the whitespace noise. A reformat, a new editor, or a tab-vs-space preference produces a huge git diff that changes nothing at runtime. This shows only the leaf paths whose values actually moved.
  • Confirm two files are semantically identical. When the trees match, you get "No differences. The two TOML documents are semantically identical." instead of having to read the whole thing.
  • Check what a generator or merge changed. Paste the original into A and the regenerated file into B to see the exact added, removed, and changed keys.
  • Ignore array re-ordering when it doesn't matter. Set Arrays to Ignore order so a features or tags list that was shuffled but not changed stops showing up as a difference.
  • Skim a large diff. Use the side-by-side view with Collapse on to hide long runs of unchanged keys and keep your eyes on the changes.
  • Produce a patch-shaped report. Switch View to Patch (TOML lines) to get -/+ sub-lines for each changed value, which reads like a unified diff.
  • Compare a secret-bearing config safely. Both files are parsed in your browser; nothing is uploaded, so it's fine for files with tokens or keys.

How the diff works

The tool runs automatically about a fifth of a second after you stop typing in either pane, and re-runs whenever you change an option, swap the inputs, or press Compare (Cmd/Ctrl+Enter also works).

1. Parse both sides

A and B each go through the same strict TOML 1.0 parser (smol-toml) the rest of this site uses. If either side is malformed, parsing throws and the status bar shows the error — there is no partial diff. If a pane is empty, you get Left input (A) is empty. or Right input (B) is empty. instead.

2. Walk the trees by leaf path

Starting at the root, the two trees are walked together. At each table, the keys from both sides are merged and sorted alphabetically, so the report is deterministic no matter what order the keys appeared in your files. A key present only in B becomes a + (with its whole value, table or scalar); a key present only in A becomes a -. When both sides have a key and both values are tables, the walk recurses into them — so a table that exists on both sides is compared field by field, while a table that's new or removed is reported as a single line.

3. Compare leaf values exactly

When both sides have a key and the values aren't both tables, they're compared by value. Strings, numbers, booleans, and datetimes match only if they're truly equal; arrays match only if they have the same elements in the same order; this comparison is type-strict, so 1 and 1.0 are different because one is an integer and one is a float. Any mismatch is recorded as a ~ change carrying both the old and the new value.

4. Summarize and render

The collected changes are counted into the header line — for example # 2 added · 1 removed · 3 changed — and then formatted according to the View option. Whitespace, comments, and key order never reach this stage, because they were dropped at parse time. That's what makes the diff semantic.

Options reference

View

Three ways to render the same set of changes. Dotted paths (the default) prints one line per change: +/- lines show the full dotted path and value, and a ~ line shows path: old → new on a single line. Side-by-side lays the two values out in two columns, each padded to about 38 characters, with the change marker in the left margin — best for scanning a wide set of changes. Patch (TOML lines) reads like a unified diff: added and removed keys are single +/- lines, and a changed key prints ~ path followed by an indented - old line and + new line.

Arrays

Order matters (the default) treats two arrays as different if their elements are in a different order, because TOML arrays are ordered and the order can be meaningful. Ignore order treats arrays as equal when they contain the same elements regardless of position — the right choice for sets-pretending-to-be-arrays like a features or tags list, where a reshuffle isn't a real change.

Collapse

Off shows the diff as-is. Hide runs of equal keys collapses a stretch of more than five consecutive unchanged keys into a single … N unchanged marker, so the changes don't get lost in a long file. This only affects the Side-by-side view — the dotted-paths and patch views already list changes only, so there is nothing for collapse to hide there.

Output: the diff format

Every diff starts with a header counting the changes, then the body. The header is always # X added · Y removed · Z changed. The body depends on the View:

Dotted paths (default)

  • + path.to.key = value — the key exists in B but not A.
  • - path.to.key = value — the key exists in A but not B.
  • ~ path.to.key: old → new — the key exists in both and its value changed.

Side-by-side

Two columns, A on the left and B on the right, each padded to about 38 characters and separated by |. The change marker (+, -, ~) sits in the left margin; an added key fills only the right column, a removed key only the left, a changed key shows both. With Collapse on, a run of more than five matching keys becomes one … N unchanged row.

Patch (TOML lines)

  • + path.to.key = value and - path.to.key = value for adds and removes, as above.
  • A changed key prints ~ path.to.key, then an indented - old line and a + new line beneath it.

When the trees match, there are no body lines and no header — instead the output is the single line No differences. The two TOML documents are semantically identical. The Download .txt button saves whatever is in the output pane as diff.txt.

Example

A (original):

[package]
name = "tomlkit"
version = "1.4.2"

[dependencies]
toml = "0.8"

B (changed):

[package]
name = "tomlkit"
version = "1.5.0"
description = "TOML utilities"

[dependencies]
toml = "0.9"
tokio = { version = "1", features = ["full"] }

Output, Dotted paths view:

# 2 added · 0 removed · 2 changed
+ dependencies.tokio = { version = "1", features = ["full"] }
+ package.description = "TOML utilities"
~ dependencies.toml: "0.8" → "0.9"
~ package.version: "1.4.2" → "1.5.0"

Notice that tokio — a key present only in B — is reported as one + line with its value rendered as an inline table, not expanded into per-leaf lines. The walk only recurses into a table when both sides have it; a brand-new table is added whole. Everything is sorted alphabetically by path, and the version bumps come out as single ~ lines showing old → new.

Recipes by intent

Review a PR to Cargo.toml or pyproject.toml

Paste the base version into A and the branch version into B. Leave View on Dotted paths and Arrays on Order matters. The header tells you at a glance how many real changes the PR makes, and each line is a leaf path you can map straight onto the dependency or setting that moved.

Prove a reformat changed nothing

Put the original in A and the reformatted file in B. A semantic diff ignores the spacing, quoting, and key-order churn a formatter introduces, so if the only edits were cosmetic you'll get "No differences. The two TOML documents are semantically identical."

Compare configs where list order doesn't matter

Set Arrays to Ignore order. A features = ["a", "b"] that became ["b", "a"] stops being flagged, leaving only the changes that matter. Keep this off when the array is a sequence whose order carries meaning (a pipeline, an ordered include list).

Skim a big diff

Switch View to Side-by-side and turn Collapse on. Long stretches of unchanged keys fold into … N unchanged rows, so the +/-/~ rows are easy to find. Use Swap A↔B if you pasted the versions in the wrong order — it switches the panes and re-runs.

Generate a patch-style report to paste elsewhere

Set View to Patch (TOML lines), then Copy or Download .txt. Each changed value appears as an indented - old / + new pair, which reads naturally in a code review comment or a changelog.

Limits and performance

  • Both files must parse. A semantic diff needs two trees. If either side is malformed, you get a parse error and no diff — run the side that fails through the TOML Validator to find the exact line.
  • Comments aren't diffed. The parser doesn't carry comments into the value tree, so a comment-only change shows as no difference.
  • Type differences count. 1 versus 1.0, or a quoted "true" versus a bare true, are reported as changes because the parsed values differ in type. That's a feature, not a bug — those differences are real at runtime.
  • Collapse is side-by-side only. The dotted-paths and patch views never print unchanged keys, so the collapse option has no effect there.
  • Two files at a time. There's no three-way diff — to compare three versions, run two pairwise diffs.
  • In-memory. Both documents, both trees, and the output all live in the tab at once. Project configs are instant; very large TOML (tens of MB) can make the textarea lag when rendering the result. Prefer Download .txt over Copy for huge diffs.

Errors and how to fix them

Left input (A) is empty. / Right input (B) is empty.

One of the two panes is blank (or whitespace-only). A diff needs both sides. Paste the matching version into the named pane — A is the left pane (original), B is the middle pane (changed) — or click Try sample to load a worked example into both.

Status bar goes red with a parse error

One of the documents isn't valid TOML 1.0, so there's no tree to compare. The error names the offending side and position. Common causes are unclosed quotes or brackets, a missing =, or a duplicate key. Paste that file into the TOML Validator, which points at the exact line and column and suggests a fix.

A diff I expected to be empty shows changes

Almost always an array re-order or a type shift. If a list was shuffled but not changed, switch Arrays to Ignore order. If a value flipped type — 1 to 1.0, or a number that got quoted into a string — that is a genuine semantic change and is reported on purpose.

The two versions are in the wrong panes (adds and removes are inverted)

The + marker always means "in B but not A" and - means "in A but not B". If the report reads backwards, click Swap A↔B to exchange the panes; it re-runs the diff immediately.

A changed table shows up as several separate lines

Expected, and useful. When a table exists on both sides, the diff recurses into it and reports per-field changes by dotted path — so editing one value inside [dependencies.tokio] shows ~ dependencies.tokio.version, not a whole-table rewrite. A table that's entirely new or removed is reported as one line instead, with its value rendered inline.

FAQ

What does "semantic" mean here, exactly?

It compares the parsed value trees, not the raw text. Whitespace, comments, key order, quote style, and inline-vs-block table layout are all dropped at parse time, so two files that produce the same config — however differently they're written — show no differences.

Does it preserve which file each line came from?

Yes. + always means "in B but not A" and - always means "in A but not B". The control bar order — A on the left, B in the middle — matches the markers.

Why are keys alphabetized instead of in file order?

Because the two files may list keys in different orders, and a stable, sorted output makes the diff deterministic and easy to re-read. Order in the source files is irrelevant to a semantic diff anyway.

Is my data uploaded?

Never. Both documents are parsed and compared entirely in your browser. You can go offline and it still works — safe for configs that hold secrets or API keys.

Can it diff three files?

Not directly. Run two pairwise diffs — A against B, then B against C.