XML special characters: escaping, entities, and CDATA
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
| Character | Write instead | Must be escaped… |
|---|---|---|
& | & | Everywhere (text and attributes) |
< | < | Everywhere (text and attributes) |
> | > | Only in the sequence ]]>; escaping it always is good practice |
" | " | Inside double-quoted attribute values |
' | ' | 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&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 < 100.
Option 2: numeric character references
Any Unicode character can be written by its code point, in decimal (é → é) or hexadecimal (é → é). This is useful for characters your editor or keyboard can’t produce, or for making invisible characters explicit — for example   for a non-breaking space.
Note that unlike HTML, XML does not know named entities like or é — 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:
- It only works in element content — never inside attribute values.
- The one thing it cannot contain is its own terminator,
]]>. The standard workaround is to split it into two CDATA sections. - It’s a convenience for humans, not a different data type: a parser reports identical text whether you used CDATA or entity escaping.
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 "yes""/>
<!-- ✗ 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 or	.
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:
- Comments —
<!-- a & b -->is fine. The one forbidden sequence is--. - CDATA sections — everything is literal text until
]]>. - Processing instructions — content runs until
?>.
Writing & in any of these produces a literal five-character string& 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; Jerry</show> <!-- ✗ renders as "Tom & Jerry" -->This happens when text passes through two escaping steps — a template escapes it, then a serialiser escapes the result again. The symptom is &,<, or " 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 (	), line feed ( ), and carriage return ( ) 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
- Escape
&and<always, in text and attributes. - Escape quotes only inside attribute values using the same quote style.
- Use CDATA for blocks of code or markup-heavy text (element content only).
- Use numeric references (
é) for exotic or invisible characters. - Never use HTML-only names like
.
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.