XML special characters: escaping, entities, and CDATA

Guide · Updated July 2026 · ~6 min read

Five characters have special meaning in XML, and using them unescaped is one of the most common ways a document becomes “not well-formed”. Here’s exactly which characters must be escaped, where, and the three ways to do it.

The five predefined entities

CharacterWrite insteadMust be escaped…
&&Everywhere (text and attributes)
<&lt;Everywhere (text and attributes)
>&gt;Only in the sequence ]]>; escaping it always is good practice
"&quot;Inside double-quoted attribute values
'&apos;Inside single-quoted attribute values

The two that break documents in practice are & and <. The parser treats every & as the start of an entity reference and every< as the start of a tag — there is no “it’ll probably be fine” mode.

The classic failure: URLs with query strings

<!-- ✗ Invalid — parser reads "&page" as a broken entity -->
<url>https://example.com?q=test&page=2</url>

<!-- ✓ Valid -->
<url>https://example.com?q=test&amp;page=2</url>

This is the single most frequent escaping error, because URLs full of &get pasted into sitemaps, RSS feeds, and config files daily. Error messages vary —“entity name must immediately follow the ‘&’” or “undefined entity” — but the fix is always the same. The Indentio XML formatterdetects bare ampersands and escapes them for you in one click.

The same rule catches comparison operators in text — <discount>price < 100</discount>must be written price &lt; 100.

Option 2: numeric character references

Any Unicode character can be written by its code point, in decimal (&#233; → é) or hexadecimal (&#xE9; → é). This is useful for characters your editor or keyboard can’t produce, or for making invisible characters explicit — for example &#160; for a non-breaking space.

Note that unlike HTML, XML does not know named entities like&nbsp; or &eacute; — using them is an “undefined entity” error unless a DTD defines them. Only the five entities in the table above are built in.

Option 3: CDATA sections

When a text block is full of special characters — an embedded code sample, an HTML snippet in an RSS feed — escaping every character is unreadable. A CDATA section tells the parser “everything here is plain text”:

<description><![CDATA[
  if (a < b && b > 0) { launch(); }
]]></description>

Three things to know about CDATA:

Escaping inside attribute values

Attributes follow slightly different rules from element text, and the difference is the source of a lot of confusion. Inside an attribute value you must escape &and < as always — and additionally the quote character you used to delimit the value. The other quote character needs no escaping:

<!-- ✓ double-quoted value containing an apostrophe -->
<book title="Ada's Notes"/>

<!-- ✓ single-quoted value containing a double quote -->
<book title='He said "yes"'/>

<!-- ✓ escaped, works with either delimiter -->
<book title="He said &quot;yes&quot;"/>

<!-- ✗ unescaped delimiter ends the value early -->
<book title="He said "yes""/>

Switching the delimiter is the cleaner fix when a value contains only one kind of quote. When it contains both, escaping is the only option. Note also that a literal newline or tab inside an attribute value is legal but is silently normalised to a single space by the parser — if you need one preserved, write it as &#10; or&#9;.

Where escaping is not needed

Three places in an XML document are not parsed as markup, so the escaping rules do not apply inside them:

Writing &amp; in any of these produces a literal five-character string&amp; in the output rather than an ampersand — which brings us to the opposite failure.

Double escaping: the other failure mode

Unescaped characters break the parser loudly. Over-escaped characters break nothing and are far harder to notice: the document parses perfectly and the data is simply wrong.

<!-- Intended text: Tom & Jerry -->
<show>Tom &amp;amp; Jerry</show>   <!-- ✗ renders as "Tom &amp; Jerry" -->

This happens when text passes through two escaping steps — a template escapes it, then a serialiser escapes the result again. The symptom is &amp;,&lt;, or &quot; appearing literally in a rendered feed, product title, or web page. The fix is never to add more escaping; it is to find and remove the duplicate step. If you are hand-escaping a string before handing it to a library that also escapes, stop doing the first one.

Do not escape by hand

Almost every escaping bug in production comes from building XML with string concatenation:

// ✗ breaks the moment a title contains &, <, or a quote
xml += "<title>" + product.title + "</title>";

Use your language’s XML serialiser instead — xml.etree.ElementTree in Python,XMLSerializer or fast-xml-parser in JavaScript,XmlWriter in .NET, JAXB in Java. They escape correctly and exactly once, which solves both this problem and double escaping in a single move. Hand-rolled escaping is also how injection vulnerabilities get into XML-producing services.

Characters you can’t use at all

XML 1.0 forbids most control characters (U+0000–U+001F) entirely — even escaped as numeric references. Only tab (&#9;), line feed (&#10;), and carriage return (&#13;) are allowed. A stray NUL orESC byte from a log file or database dump makes the document unparseable, and because the characters are invisible, you’ll want a validator that reports the exact line and column to find them.

Cheat sheet

Unsure whether a document is escaped correctly? Paste it into theXML validator — every bare ampersand and stray angle bracket is underlined at its exact position, entirely in your browser.

Keep reading