XML Diff
Compare two XML documents by structure — added, removed and changed elements, attributes and text, addressed by node path — entirely on this device.
Defaults: attribute order ignored, names compared by namespace URI, whitespace between elements ignored, comments and processing instructions not compared. Sibling order is always significant in XML, so a moved element is one removed plus one added.
Drop an XML file into either side — it is read on this device and never uploaded.
Press ? for shortcuts
Do it offline:$ diff <(xmllint --c14n a.xml) <(xmllint --c14n b.xml)
How to use it
- Paste the baseline document into side A and the candidate into side B; a file dropped on either side is read on this device, never uploaded.
- Read the summary strip as soon as both sides parse: it counts added, removed and changed nodes, plus attribute changes.
- Keep the defaults shown above the inputs — attribute order ignored, names by namespace URI, whitespace between elements ignored, comments and processing instructions not compared — or switch any when the detail matters.
- Step through the list with the next and previous controls; selecting a row highlights its exact source range in the side it lives on.
- Copy or download the change report, or share the comparison as a link — past 8 KB of compressed state you get a state file instead.
Two XML files can differ on every line and still carry the same data. This page parses both sides and compares the documents themselves — element order, attribute values, text and namespace-resolved names. Re-indentation and attribute order become notes instead of changes, and every real difference is listed at its own node path.
Examples
<config env="prod" region="eu-west-1"><timeout>30</timeout></config>XML structural comparison
0 added · 0 removed · 0 changed
No differences
The attributes swapped places and the file gained indentation. Neither is data: a start tag is an unordered attribute set, and the newline between config and timeout sits in element-only content. Verdict: no differences, both suppressions named beside the summary.
<catalog><item sku="A1"><price>10.00</price></item><item sku="B2"><price>20.00</price></item></catalog>XML structural comparison
1 added · 0 removed · 1 changed
~ /catalog/item[1]/price/text() — Text changed: 10.00 → 12.50
+ /catalog/item[2]/stock — Added: <stock>4</stock>
One price moved and one child element appeared. Repeated siblings are addressed by position, so item[1] and item[2] stay apart, and the text node carries its own step instead of folding into its parent.
<a xmlns:p="urn:x"><p:b/></a>XML structural comparison
0 added · 0 removed · 0 changed
No differences
Edge case: one element, two prefixes bound to the same namespace URI. A prefix is a local nickname, so the default compares URIs and the pair matches. Switch names to compare by prefix and the same pair reports one renamed element.
<server host="db1" port="5432"/>XML structural comparison
0 added · 0 removed · 0 changed · 2 attributes
~ /server/@port — Attribute changed: port="5432" → port="5433"
+ /server/@tls — Attribute added: tls="on"
Attributes compare as a set keyed by resolved name, so each is reported against its own path: port took a new value, tls did not exist before, host matched silently. The element is unchanged, so node counts stay at zero.
Every example on this page is executed against the same engine that powers the tool, on every build.
Gotchas worth knowing
Sibling order is data
Child elements are a sequence, not a set: moving one past another is a removal plus an addition, and no option turns that off. Attributes are the opposite case, which is why only they are normalized.
Whitespace counts inside mixed content
A newline between two child elements is layout, so it is dropped. The same space inside an element holding text and children at once belongs to the text and is kept: Signed <b>here</b> against Signed<b>here</b> reports a change.
A namespace declaration reaches every descendant
Adding xmlns to a parent renames every element under it, so one edited line can legitimately produce changes far below. A prefix with no declaration in scope resolves to nothing: it is compared as written, and a note says so.
Spellings that carry the same characters match
An empty element written both ways, a CDATA section against the escaped text it stands for, and a character reference against the character it names all compare equal, each behind a note. An unresolvable reference stays literal.
Where the limits sit
Each side accepts 50 MB and the pair two million nodes; both bounds refuse rather than truncate. Past 5,000 differences the list is cut with a note while the summary still counts them all.
Common errors
Closing tag '</config>' at line 1, column 20 — '<timeout>' opened at line 1, column 9 is still open; did you forget '</timeout>'?- A tag left open swallows the rest of the document, so the mismatch is reported where it becomes visible, naming the tag still open. Only that side stops.
Bare '&' at line 1, column 31 — probable cause: a literal ampersand; write '&' (entities look like '&name;', ' ' or '😀').- Query strings pasted into element content are the usual source. The comparison resumes the moment that side parses again.
No root element at line 1, column 50 — probable cause: the document has only a prolog (declaration, comments or DOCTYPE); an XML document needs exactly one element.- A declaration, a comment and a DOCTYPE are all prolog: none is the one element a document needs. An empty or blank side is different — it waits, with no error at all.
The two documents hold 2,000,002 nodes together, over the 2,000,000-node limit for this comparison.- The combined node guard, counted once both sides parse. The pair is declined whole, with both exact totals printed.
Side A is larger than the 50 MB limit for this tool. For bigger documents use the CLI: diff <(xmllint --c14n a.xml) <(xmllint --c14n b.xml)- The size refusal, raised before parsing starts, naming this page's own command line. Refusing never clears either side.
FAQ
Why do swapped attributes report no differences?
Because a start tag carries an unordered attribute set: XML fixes no order between two attributes of one element, so writing them the other way round changes the file, not the document. The suppression is stated, never silent — a note says the order differed, and setting attribute order to compared reports the first out-of-order attribute.
Is this the same thing as canonical XML?
No. Canonical XML rewrites a document into one exact serialisation — attribute order, empty elements, namespace declarations — so that signatures compare. This page normalises only what it lists on screen and reports everything else, including the whitespace canonicalisation keeps. Where the two agree, the command line below returns the same verdict.
Do both documents have to use the same namespace prefixes?
No. Names resolve to their namespace URI before anything is compared, so p:item and q:item match whenever both prefixes are bound to one URI, and the same prefix bound to two different URIs does not. Set names to compare by prefix when the literal spelling is what you are reviewing.
Keyboard shortcuts
- ⌘/Ctrl + Enter
- Compare again, at any input size.
- ⌘/Ctrl + S
- Download the change report.
- ⌘/Ctrl + C
- Copy the report when nothing is selected.
- ⌘/Ctrl + Shift + C
- Copy the report despite a selection.
- s
- Swap the two sides; one undo puts them back.
- n
- Go to the next difference.
- p
- Go back one difference.
- t
- Cycle to the next theme.
- ? or ⌘/Ctrl + /
- Show the keyboard shortcuts.
- Esc
- Close the topmost dialog; both sides keep their text.