XML formatter / validator
Pretty-print, minify and check XML in your browser, with well-formedness errors reported by line and column, and comments, CDATA sections, processing instructions and mixed content all kept intact.
Document summary
- Root element
- <catalog>
- Elements
- 12
- Attributes
- 7
- Nesting depth
- 4
The document is well-formed. That is a syntax check, not a check against a DTD or an XML Schema.
Elements are indented by depth. An element that holds text next to other elements, and anything under xml:space="preserve", is left on one line exactly as written, because the spaces inside it are part of the data.
Pretty-print, minify and check XML in your browser, with well-formedness errors reported by line and column, and comments, CDATA sections, processing instructions and mixed content all kept intact.
On this page
How to use this tool#
- Paste your XML over the example. The example is there so you can see the shape of the output before committing anything of your own. Formatting runs as you type, so there is no button to press.
- Pick a direction. Pretty-print indents the document by nesting depth. Minify strips the whitespace between elements and puts the document on one line.
- Choose the indentation. 2 spaces, 4 spaces or a tab. The choice only applies to pretty-printing.
- Read the message under the output. If the document is not well-formed you get the first problem with its line and column, and the offending line with a caret under the exact character. If it is well-formed you get a summary instead: root element, element count, attribute count and nesting depth.
- Use Swap to check a round trip. It puts the output back in the input box and flips the direction, so you can minify what you just formatted and confirm you get the same document back.
The tool doubles as an XML validator in the everyday sense of the word: it tells you whether the document parses. That is a check ofwell-formedness, not of validity against a DTD or an XML Schema. The difference is explained below, and it matters more than it sounds.
Everything happens in your browser after the page has loaded. Nothing is uploaded, there is no backend to upload it to, and the page makes no network request while you type, so a configuration file with credentials in it is safe to paste.
What the formatter changes, and what it leaves alone#
A formatter that rewrites your data is worse than no formatter. This one moves exactly one thing: the whitespace that sits between two pieces of markup. Everything else comes back character for character, including the parts most tools quietly drop.
<?xml version="1.0" encoding="UTF-8"?>
<config><server host="localhost" port="8080"><timeouts connect="5s" read="30s"/></server>
<logging level="info"><!-- raise to debug when reproducing a bug --><file>/var/log/app.log</file></logging>
<note>Set <code>level</code> to <code>debug</code> locally.</note></config><?xml version="1.0" encoding="UTF-8"?>
<config>
<server host="localhost" port="8080">
<timeouts connect="5s" read="30s"/>
</server>
<logging level="info">
<!-- raise to debug when reproducing a bug -->
<file>/var/log/app.log</file>
</logging>
<note>Set <code>level</code> to <code>debug</code> locally.</note>
</config>Look at what survived that:
- The comment is still there, on its own line with its siblings. Python's
ElementTreethrows comments away unless you build the parser withTreeBuilder(insert_comments=True), so a parse-and-write round trip through it loses them silently. - The XML declaration is kept as it was. Its internal spacing is normalized to one space between parts, and nothing else about it changes. A document with no declaration does not get one added.
- The
<note>paragraph stays on one line. It holds text next to elements, which makes every space in it part of the content. Breaking it across lines would change what it says. - Attribute order, quote characters and values are untouched. If you wrote
host='localhost'with single quotes, single quotes come back. The run of spaces betweenhostandportcollapses to one, because whitespace inside a tag is not data. <c></c>does not become<c/>. The two are the same to a parser, but they are not the same to a diff, so the form you wrote is the form you get back.xmllint --formatrewrites the long form into the short one.
Minifying the same file gives this, which is byte for byte what a parser sees anyway:
<?xml version="1.0" encoding="UTF-8"?><config><server host="localhost" port="8080"><timeouts connect="5s" read="30s"/></server><logging level="info"><!-- raise to debug when reproducing a bug --><file>/var/log/app.log</file></logging><note>Set <code>level</code> to <code>debug</code> locally.</note></config>Whitespace in XML is data, unless it sits between two tags#
This is the whole problem of XML formatting in one sentence, and it is the reason a lot of pretty-printers corrupt documents. XML has no concept of insignificant whitespace of its own. A space inside an element is character data exactly like a letter is. Only a schema can say that a particular run of spaces was decoration, and a formatter has no schema.
What a formatter can say safely is narrower: whitespace that sits between two pieces of markup, with no other text anywhere in that element, is layout that somebody typed to make the file readable. Everything else has to be left exactly where it is.
<!-- Between two tags: layout. Moved freely. -->
<list>
<item>a</item>
<item>b</item>
</list>
<!-- Around an inline element: data. Left alone. -->
<p>See <a href="#x">this note</a> for details.</p>
<!-- The whole content of an element: data. Left alone. -->
<indent> </indent>
<!-- Under xml:space="preserve": data, all of it. -->
<pre xml:space="preserve">
def main():
return 0
</pre>The failure case is mixed content: an element holding text and child elements at once, which is how XML expresses a sentence with markup inside it. Python'sxml.dom.minidom indents those text nodes like any other child, which inserts newlines and spaces into the middle of a sentence.
Source
<p>Hello <b>world</b>!</p>
Python 3.11, xml.dom.minidom.toprettyxml(indent=" ")
<p>
Hello
<b>world</b>
!
</p>
The two are not the same document. The first paragraph reads
"Hello world!", the second reads "\n Hello \n world\n !\n ".
xmllint --format, ElementTree.indent and this tool all leave it alone:
<p>Hello <b>world</b>!</p>The other case a formatter must respect is xml:space="preserve". That attribute is defined by the XML specification itself, and it says the whitespace inside this element is intentional. Here nothing under it is touched, and it stays in force for descendants until anxml:space="default" turns it off again.
<doc>
<pre xml:space="preserve">line one
line two</pre>
<p>after</p>
</doc>Well-formed is not the same as valid#
Both words have exact meanings in the XML specification, and mixing them up is how people end up surprised by a tool.
- Well-formed means the document obeys XML's syntax: one root, tags that nest and match, quoted attributes, escaped
&and<. Any XML parser can check this with no other file, and a parser must refuse a document that fails. This tool checks well-formedness. - Valid means the document also matches a grammar declared somewhere else: a DTD, an XML Schema (XSD), RELAX NG or Schematron. That grammar is what says a
<book>must contain exactly one<title>, that@yearholds an integer, or that@currencyis one of three strings. Checking it needs the schema, and often needs to fetch it.
A document can be well-formed and completely wrong for its purpose. Nothing in XML syntax stops you writing <invoice><colour>blue</colour></invoice>. If you need the second kind of check, use xmllint --schema, Java'sjavax.xml.validation, Python's lxml, or whatever your XSD lives next to. A page with no backend cannot fetch your schema, so it cannot do it honestly.
These are the rules this tool enforces, in full:
1. Exactly one root element wraps everything.
2. Every start tag has a matching end tag, or is written <tag/>.
3. Tags nest, never overlap: <a><b/></a> not <a><b></a></b>
4. Tag and attribute names are case sensitive: <Note> needs </Note>.
5. Every attribute value is quoted, with " or ', and no name repeats
on the same tag.
6. < and & never appear raw in text; < never appears in an attribute
value either.
7. Every & starts a reference that a parser can resolve.
8. Names start with a letter, "_" or ":", never with a digit.
9. Comments contain no "--" and do not end with "--->".The five escapes, and the two people get wrong#
XML defines exactly five named entities and no others. Everything else in the HTML entity list you have memorised, first among them, comes from HTML's own DTD and does not exist in plain XML.
< < always, in text and in attribute values
& & always, in text and in attribute values
> > only needed after "]]", legal everywhere else
" " only inside a value delimited by "
' ' only inside a value delimited by '
    any character, by decimal or hex code point
NOT XML. That name comes from HTML's DTD.Two of those lines catch people out. > does not need escapingin ordinary text: 2 > 1 is well-formed XML, and this tool accepts it. The one place it does need escaping is immediately after ]], because the sequence]]> is reserved as the CDATA closer and is banned in character data. And is an error, not a warning. Write  or  , or paste the character itself, or declare the name yourself.
<!DOCTYPE catalog [
<!ENTITY company "Example Ltd">
<!ENTITY nbsp " ">
]>
<catalog>
<owner>&company;</owner>
<gap>a b</gap>
</catalog>A <!ENTITY> in the internal subset of the DOCTYPE declares a name for the rest of the document. This tool reads those declarations, so a reference to one you declared passes. It never expands them: &company; comes out of the formatter as&company;, because expanding it would change the document.
If the DOCTYPE points at an external DTD with SYSTEM orPUBLIC, the declarations could be in a file this page cannot fetch. In that case an unknown entity is reported as a note rather than an error, since a non-validating processor is not required to read the external subset either.
CDATA sections#
A CDATA section switches off markup recognition for a stretch of text, which is how you embed source code, an SVG path, or a chunk of HTML without escaping every character. It is a convenience, not a data type: a parser hands the contents back as ordinary character data, and the section boundaries are gone by the time your code sees it.
<script><![CDATA[if (a < b && b > c) { return "<b>"; }]]></script>
<!-- A CDATA section cannot contain "]]>", so split it across two: -->
<sample><![CDATA[end with ]]]]><![CDATA[> here]]></sample>The one thing a CDATA section cannot contain is its own terminator, ]]>. There is no escape character inside one, so the standard workaround is to close the section between the brackets and the angle bracket and open a new one, as above. This tool keeps CDATA sections exactly as written in both directions, including the split.
The declaration, and what encoding really depends on#
<?xml version="1.0" encoding="UTF-8"?>
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<?xml version="1.0"?>
Rules the declaration has to follow:
- nothing before it, not even a space or a comment
- the three parts in this order: version, encoding, standalone
- version first and required, standalone only "yes" or "no"
Not a declaration, and not allowed in the middle of a document:
<?xml version="1.0"?> as a processing instruction
<?XML version="1.0"?> "xml" is reserved in any caseThe declaration is optional. When it is absent and the file has no byte order mark, a parser is required to read the bytes as UTF-8. That is the source of the single most common XML failure in the wild: a file saved as Windows-1252 or ISO 8859-1 with an accented character in it, and no declaration to say so. xmllint stops withInvalid bytes in character encoding and names the byte.
The encoding pseudo-attribute is a statement about the bytes on disk. By the time text reaches a box on this page the browser has already decoded it, so changing the declaration here changes a label and nothing else. To actually change a file's encoding, useiconv or your editor's "save with encoding", then fix the declaration to match.
A byte order mark pasted in at the start is dropped, with a note, because it is not part of the document's character data. Carriage returns are normalized to line feeds, which is what an XML processor is required to do before anything else sees the text.
Namespaces are a separate specification#
XML Namespaces is a layer on top of XML, published separately. The core XML rules know nothing about it: to them, s:Envelope is simply a name with a colon in it, and a colon is a legal name character. This is why a document with an undeclared prefix is still well-formed XML while being broken for anything namespace-aware.
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope">
<s:Body>
<m:Get xmlns:m="https://example.com/x">
<m:Id>7</m:Id>
</m:Get>
</s:Body>
</s:Envelope>So this tool treats it the way the specifications do. A prefix used without a matchingxmlns: declaration in scope gets a note under the output, not an error, and the document still formats. xmllint draws the same line: it prints "Namespace prefix p on a is not defined" and still exits 0.
Two prefixes are bound by the Namespaces specification itself and never need declaring:xml:, which is how xml:space and xml:lang work, andxmlns: itself.
What minifying actually saves#
Minifying XML removes the whitespace between elements and nothing else. On a deeply nested document that is worth having: a 915 byte hand-indented Maven POM minified here comes to 740 bytes, 19 percent smaller, and a small SOAP envelope loses 10 percent. On a flat document with long text values it saves almost nothing.
Two things worth knowing before you minify something in production:
- Transport compression usually beats it. Indentation is the most compressible thing in a file, being the same few characters over and over. If the response already goes out gzipped or brotlied, minifying first buys far less than the byte count suggests.
- A signed document must not be touched. XML Digital Signature covers a canonical form of the content, and Canonical XML keeps whitespace rather than removing it, so reformatting a signed document in either direction invalidates the signature. Minify before signing or not at all.
Comments are kept when minifying here. If you want them gone, that is a content decision rather than a whitespace one, so strip them deliberately with xmlstarlet ed -d '//comment()'or an XSLT template rather than hoping a formatter does it.
The errors you are most likely to hit#
Every message names the line and the column and shows the line with a caret under the exact character. These three cover most of the real traffic:
<order id="7">
<customer>Ben & Jerry</customer>
</order>
An "&" starts a reference, so a bare ampersand has to be written &.
No closing ";" was found (line 2, column 17)
2 | <customer>Ben & Jerry</customer>
| ^
---
<note>
<to>Tove</to>
<from>Jani</From>
</note>
</From> closes an element that is not open. <from> was opened on line 3
and is still waiting for </from>. Tag names are case sensitive
(line 3, column 13)
3 | <from>Jani</From>
| ^
---
<item sku="A1" qty="2" sku="A2"/>
The attribute "sku" is set twice on the same <item> tag (line 1, column 24)
1 | <item sku="A1" qty="2" sku="A2"/>
| ^The second one is worth staring at. XML is case sensitive everywhere: element names, attribute names and the DOCTYPE keyword. <from> and </From> are two different names, and no parser will guess what you meant. If you are copying markup out of HTML, expect this one repeatedly, along with unclosed <br> and<img> tags, which XML requires you to write as <br/> and<img/>.
Formatting XML on the command line#
xmllint
Part of libxml2, and already installed on most Linux and macOS machines. Checked here against libxml 2.15.3.
# Pretty-print. Two spaces, and it adds an XML declaration if there is none.
xmllint --format input.xml
# Four spaces instead, through the environment variable.
XMLLINT_INDENT=" " xmllint --format input.xml
# Minify: drop the whitespace between elements.
xmllint --noblanks input.xml
# Check only. The message goes to stderr, and the exit status is
# 0 well-formed, 1 usage error, 3 invalid, 4 not well-formed.
xmllint --noout input.xml
# Well-formedness plus validity, against a DTD or a schema.
xmllint --noout --valid input.xml
xmllint --noout --schema schema.xsd input.xmlNote the two behaviours that differ from this page: --format adds an XML declaration to a document that had none, and rewrites <c></c> as<c/>. Both are harmless to a parser and both show up in a diff.
Python
import xml.etree.ElementTree as ET
# ET.indent() is Python 3.9 and later. Comments are dropped unless the
# parser is told to keep them, and the declaration unless it is asked for.
parser = ET.XMLParser(target=ET.TreeBuilder(insert_comments=True))
tree = ET.parse("input.xml", parser=parser)
ET.indent(tree, space=" ")
tree.write("output.xml", encoding="utf-8", xml_declaration=True)
# Check well-formedness only.
try:
ET.parse("input.xml")
except ET.ParseError as error:
print(error) # "mismatched tag: line 3, column 13"# Avoid for anything with mixed content:
import xml.dom.minidom
print(xml.dom.minidom.parse("input.xml").toprettyxml(indent=" "))
# It indents every text node, so "Hello <b>world</b>!" comes back as four
# lines of text with new spaces and newlines inside it. Checked with
# Python 3.11.14.JavaScript in the browser
// In a browser. There is no XML parser in Node's standard library,
// so this only runs on a page.
const doc = new DOMParser().parseFromString(text, 'application/xml');
// A failure is not an exception: it is a document containing a
// <parsererror> element, so the check has to be explicit.
if (doc.querySelector('parsererror')) {
throw new Error('Not well-formed XML');
}
// Back to text. No indentation is added: XMLSerializer writes the tree
// out as it stands.
const out = new XMLSerializer().serializeToString(doc);This tool does not use DOMParser. It has its own parser, so the error text, the line numbers and the column numbers are the same in every browser, and so that comments, CDATA sections and the declaration can be written back out unchanged, which a parse-and-reserialize round trip does not guarantee.
Questions people ask#
Is my XML uploaded anywhere?
No. The parsing and the formatting are JavaScript running in your browser, on this page, after it has loaded. The site is static files with no backend, so there is nowhere for the text to be sent. Disconnect from the network and the tool keeps working, which is the quickest way to prove it to yourself.
Is this an XML validator?
It validates syntax, which is the check people usually mean. If the document is well-formed you get the formatted output and a summary; if it is not, you get the first problem with its line and column. It does not check the document against a DTD or an XML Schema, so it cannot tell you that a required element is missing. For that you need the schema file, which a page with no backend cannot fetch. Use xmllint --schema or your language's XSD validator.
Why did my fail?
Because it is an HTML entity, not an XML one. XML predefines only&, <, >, "and '. Replace it with  , paste the character itself, or declare <!ENTITY nbsp " "> in a DOCTYPE at the top of the file.
Why is my paragraph not indented like everything else?
Because it contains text next to elements. In that case every space in it belongs to the content, so adding line breaks and indentation would change the data. The element is written back on one line exactly as you wrote it. The same applies to anything underxml:space="preserve".
Does it change my attribute values?
No, they come through character for character, with the quote character you used. Be aware that a real XML processor does not leave them quite alone: attribute-value normalization replaces any tab or newline inside a value with a single space before an application ever sees it, so a value written across two lines arrives joined by a space. This tool shows you the file, not the parser's view of it.
Can it format HTML?
Only if the HTML happens to be well-formed XML, which most is not. HTML allows unclosed tags, bare attributes such as disabled, unquoted values and , and all four are errors here. XHTML is XML by design and works. If what you have is HTML, use an HTML formatter, because an XML parser is required to reject it rather than repair it.
How large a file can it handle?
It is parsed in your browser, so the limit is your machine rather than an upload size. A few megabytes is comfortable. The output box shows the first 200,000 characters to keep scrolling smooth, and the copy button gives you the whole thing. Nesting deeper than 1000 elements is refused with a clear message rather than being allowed to exhaust the stack.
Will formatting break an XML signature or a hash?
Yes, assume it will. Any digest over the file bytes changes the moment whitespace moves, and XML Digital Signature's canonical form does not normalize whitespace inside elements either. Format before signing, never after.
What about XML 1.1?
A declaration saying version="1.1" is accepted, and so is the rest of the document, because the two versions differ mostly in which control characters and name characters they allow. XML 1.1 is rare in practice and many tool chains only implement 1.0, so unless something specifically asked you for it, 1.0 is the safer thing to write.
Where do JSON and YAML fit?
If you are converting rather than formatting, theYAML to JSON converter and theJSON formatter are next door. XML carries things neither of them has, namely attributes, comments, namespaces and mixed content, so a mechanical XML to JSON conversion always has to invent a convention for them. That is why it is a separate decision rather than a button here.
Related tools
Format, minify and validate JSON in your browser, with syntax errors explained by line and column, repeated keys listed, and number literals kept exactly as written.
Convert YAML to JSON and JSON back to YAML in your browser, following YAML 1.2, with anchors and merge keys resolved, multi-document streams, and syntax errors reported by line and column.
Convert CSV to JSON and JSON back to CSV in your browser, following RFC 4180, with the delimiter detected, quoted fields and line breaks handled, optional type inference, and errors reported by line and column.
Encode text or a file to Base64 and decode it back, with correct UTF-8, standard and URL-safe alphabets, optional padding, MIME line wrapping and data: URIs.