XML guide

Validating and Formatting XML: Well-Formed, Valid, and Safe

The difference between well-formedness and schema validity, the five escapes that actually matter, how namespaces break XPath, why pretty-printing can change meaning, and how to shut off XXE.

XML carries two separate correctness questions that people routinely merge into one. Is the document well-formed — does it parse at all? And is it valid — does it match a declared schema? A tool can answer the first for any document and the second only if you supply the schema. Getting the distinction right, plus knowing what namespaces and whitespace do to your queries, covers most of what goes wrong with XML in practice.

Well-formed and valid are different claims

Well-formedness is a syntactic property defined entirely by the XML specification. A well-formed document has exactly one root element, every start tag has a matching end tag at the correct nesting level, every attribute value is quoted, attribute names are unique within their element, and no raw `<` or `&` appears in character data. A parser either accepts the document or reports a fatal error; there is no partial success, and the specification requires the parser to stop rather than recover. That strictness is deliberate, and it is the main thing XML has over HTML.

Validity is a semantic property defined by an external schema. It asks whether `<order>` may contain `<lineItem>`, whether `quantity` must be a positive integer, whether `shippedOn` is optional. A document can be flawlessly well-formed and entirely wrong for its purpose. Conversely a document that fails well-formedness cannot be validated at all, because there is nothing to validate against — fix the syntax first.

QuestionNeedsCatches
Does it parse?Nothing but the documentUnclosed tags, raw `&`, two roots, bad encoding
Does it match the contract?An XSD, DTD or RELAX NG schemaMissing elements, wrong types, bad cardinality
Does it satisfy business rules?Schematron, or code"If type is `credit`, `iban` is required"
Is the value at this path correct?An XPath expressionOne specific field, quickly
Is it safe to parse?Parser configuration, not the documentXXE, entity expansion, external DTD fetches

Escaping: five entities and two contexts

XML predefines exactly five entities: `&lt;`, `&gt;`, `&amp;`, `&quot;` and `&apos;`. Everything else — `&nbsp;`, `&copy;`, the several hundred HTML entities people expect — is undefined in XML unless a DTD declares it. This is the single most common cause of a document that a browser renders and a parser rejects: it was written as if it were HTML. Use a numeric character reference such as `&#160;` instead, which always works.

Which escapes are required depends on the context. In character data you must escape `<` and `&`. You do not need to escape `>` except in the specific sequence `]]>`, though escaping it anyway is harmless and common. In an attribute value you must escape `<`, `&`, and whichever quote character delimits the value; the other quote may appear literally, which is why `title='He said "no"'` is legal.

CharacterReferenceIn element textIn an attribute value
`<``&lt;`RequiredRequired
`&``&amp;`RequiredRequired
`>``&gt;`Only inside `]]>`Not required
`"``&quot;`Not requiredRequired if the value uses double quotes
`'``&apos;`Not requiredRequired if the value uses single quotes
`&nbsp;` and other HTML entitiesnot defined in XMLUse `&#160;` insteadUse `&#160;` instead
Control bytes 0x00–0x08, 0x0B, 0x0C, 0x0E–0x1Fno legal formIllegal even as `&#7;`Illegal even as `&#7;`

Namespaces, and the query that returns nothing

A namespace binds a prefix to a URI, and it is the URI that establishes identity. The prefix is arbitrary local shorthand: `<soap:Envelope xmlns:soap="...">` and `<s:Envelope xmlns:s="...">` are the same element if the URIs match, and two documents using the same prefix for different URIs share nothing. The URI is an identifier, not an address; nothing fetches it, and it need not resolve.

A default namespace declaration, `xmlns="urn:example:catalog"`, applies to that element and its unprefixed descendants. It does not apply to attributes. An attribute with no prefix is in no namespace at all, ever. That asymmetry is genuinely surprising the first time and explains a large share of confused schema errors: in the example below, `book` is in the catalog namespace while its `id` attribute is not.

The practical consequence is the query that silently returns nothing. XPath 1.0 has no concept of a default namespace: an unprefixed name in an expression means "no namespace". So `//book` does not match `<book>` inside a document with a default namespace, because those are different names. The correct fix is to register a prefix with your XPath evaluator and write `//c:book`. The expedient fix, `//*[local-name()='book']`, ignores namespaces entirely and will also match a `book` from an unrelated vocabulary — fine for exploration, wrong for production.

Namespaces in a formatted document
<?xml version="1.0" encoding="UTF-8"?>
<catalog xmlns="urn:example:catalog" xmlns:m="urn:example:meta">
  <book id="b1" m:added="2026-03-01">
    <title>Sword &amp; Honour</title>
    <price currency="GBP">12.99</price>
  </book>
</catalog>

Resolved identities:
  catalog, book, title, price   -> {urn:example:catalog}name
  m:added                       -> {urn:example:meta}added
  id, currency                  -> no namespace at all

XPath against this document:
  //book                        -> nothing; "book" means no-namespace book
  //c:book   with c bound to urn:example:catalog   -> the book element
  //*[local-name()='book']      -> the book element, and any other vocabulary's
  //book/@id                    -> would also fail, for the same reason
  //c:book/@id                  -> the attribute; note @id needs NO prefix

Mixed content, whitespace, and why formatting is not free

In JSON, whitespace between tokens is meaningless and a formatter can add as much as it likes. XML has no such guarantee, because XML documents can contain mixed content: an element holding both text and child elements. In `<p>Hello <b>world</b>!</p>` the strings `Hello ` and `!` are text nodes, and the space before `<b>` is part of the data.

A pretty-printer that indents children inserts whitespace-only text nodes between them. For element-only content — a configuration file, a SOAP envelope — nobody notices, because no consumer looks at those nodes. For mixed content it changes the document: `<p>Hello<b>world</b></p>` re-indented onto three lines now has a newline and spaces where there were none, and rendering it produces different text. Good formatters detect mixed content and leave those elements on one line; not all do.

The practical rule: reformat XML freely when you are reading it, and treat the formatted version as a view rather than as the document. If the XML is signed, do not reformat it at all. XML Signature covers a canonicalised form, and although canonicalisation normalises some things, it does not normalise the whitespace inside mixed content — reindenting a signed document is a reliable way to invalidate the signature.

Schema languages, and which one answers your question

DTD is the original, built into XML itself. It can constrain element nesting and declare entities and default attribute values, but it has no datatypes and no awareness of namespaces, and its syntax is unlike XML. It survives mainly in older vocabularies and in the entity declarations that make XXE possible.

XSD (W3C XML Schema) is the industrial standard and what almost every enterprise integration means by "the schema". It is namespace-aware, has a rich datatype system that other specifications borrow, and supports type derivation and substitution groups. It is also verbose and has a steep learning curve, and expressing "exactly one of these two elements" can be awkward. XSD 1.1 added assertions and conditional type assignment, which closes some of that gap, but tool support is thinner than for 1.0.

LanguageNamespace-awareDatatypesBest at
DTDNoNoneLegacy vocabularies, entity declarations
XSD 1.0YesRich, widely reusedEnterprise integration; the default expectation
XSD 1.1YesRich, plus assertionsConditional constraints, where tools support it
RELAX NGYesVia XSD datatypesReadable grammars, document formats
SchematronYesN/A — rules, not a grammarCo-occurrence rules and human-readable messages

XXE and entity expansion

XML's entity mechanism lets a document declare an entity whose replacement text is fetched from a URI. If a parser honours that declaration, a document supplied by an attacker can read local files, reach internal network endpoints that the parser can see but the attacker cannot, and exfiltrate the contents through a second request. This is XML External Entity injection, and it has appeared in essentially every language's default XML stack at some point.

The defence is not input filtering; it is parser configuration, and it must be applied at the parser rather than at the document. Disable DTD processing entirely if you can, and disable external general and parameter entities and external DTD loading if you cannot. In Java, set `disallow-doctype-decl` on the factory. In Python, use `defusedxml` rather than the standard library parsers. In .NET, set `XmlResolver` to null. In PHP with libxml, disable entity substitution. Modern versions of several of these are safe by default, but "modern" is doing real work in that sentence and the safe default is not universal.

Entity expansion is the related availability problem. The billion-laughs attack nests ten entities, each referring to the previous one ten times, so a few hundred bytes expand to gigabytes and exhaust memory. No external access is needed, so a parser that has disabled external entities but still processes internal DTDs remains exposed. Most mature parsers now cap expansion depth and total expanded size; verify that yours does rather than assuming.

The shape of an XXE payload
<?xml version="1.0"?>
<!DOCTYPE order [
  <!ENTITY leak SYSTEM "file:///etc/passwd">
]>
<order>
  <note>&leak;</note>
</order>

A parser that resolves external entities substitutes the file's contents
into <note>. If the application echoes the parsed value back, or forwards
it anywhere, the file has left the host.

The variant that needs no echo uses a parameter entity in an external DTD
to build a URL containing the file contents and request it — the data
leaves via the request itself.

Nothing in the document is malformed. The fix is entirely in the parser:
  Java     factory.setFeature(
             "http://apache.org/xml/features/disallow-doctype-decl", true)
  Python   use defusedxml
  .NET     reader settings: DtdProcessing.Prohibit, XmlResolver = null
  libxml2  do not enable substitution of entities; load no network entities

What to remember

  • Separate the two questions: a validator with no schema reports well-formedness only, so if the document parses but a partner rejects it, ask them for the XSD.
  • XML defines only five entities; use numeric references like `&#160;` instead of `&nbsp;`, and remember that control bytes below 0x20 other than tab, LF and CR are illegal even when written as character references.
  • When an XPath returns nothing, suspect a default namespace first — bind a prefix and use it, rather than falling back to `local-name()` in production code.
  • Do not reformat XML that is signed or that contains mixed content; treat a pretty-printed version as a view of the document, not as the document.
  • Turn off DTD processing and external entity resolution in every parser that reads input you did not author, and confirm your parser caps entity expansion.

Continue with related checks and tools