XML namespaces, demystified
Two vocabularies land in the same document and both call something title. That is the whole problem namespaces solve, and the solution is one attribute. The confusion around that attribute is out of all proportion to its size, and nearly all of it traces back to one design decision: the part you type is not the part that counts.
This is the piece of XML that unlocks XSD, SOAP, Atom and XPath, because every one of those fails in a confusing, silent way when namespaces are misunderstood. What follows is the model, the clauses it comes from, and the four places it reliably surprises people.
#The problem: two vocabularies, one document
RSS 2.0 defines a title element. So do Atom, Dublin Core, SVG, XHTML and roughly every other XML vocabulary anyone has written, because title is the obvious word for the thing it names. Merge any two into one file and a schema, an XPath expression or a piece of application code has no way to tell which title it is holding.
Namespaces in XML 1.0 (Third Edition), a W3C Recommendation dated 8 December 2009, fixes this by making every element and attribute name two parts rather than one. The full name of an element, what the specification calls its expanded name, is the pair (namespace name, local name): a URI reference, plus the part you recognise. So {http://purl.org/dc/elements/1.1/}title and {http://www.w3.org/2005/Atom}title are two different names that happen to share five letters, and a processor keeps them apart without either vocabulary having to rename anything.
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:content="http://purl.org/rss/1.0/modules/content/">
<channel>
<title>Release notes</title>
<item>
<title>Version 4.2</title>
<dc:title>Version 4.2</dc:title>
<dc:creator>A. Engineer</dc:creator>
<content:encoded><![CDATA[<p>Fixes the parser.</p>]]></content:encoded>
</item>
</channel>
</rss>A document never writes an expanded name out in full. It writes a short prefix and declares, once, what that prefix stands for. Everything that goes wrong with namespaces goes wrong in the gap between those two facts.
#The prefix is a nickname. The URI is the name.
Section 3 of the Namespaces Recommendation defines exactly two forms of declaration: an attribute named xmlns:something (production PrefixedAttName) binds that prefix, and a plain xmlns (production DefaultAttName) sets the default. The prefix must be an NCName, an XML name containing no colon, which is the entire reason NCName exists as a separate production.
The prefix is local to the document, and in fact to the subtree. Two files using different prefixes for the same URI are, to any conforming processor, saying the same thing. There is no registry of prefixes and no way for a schema to require one. Everyone writes dc for Dublin Core out of habit, not because anything says they must.
<a:book xmlns:a="urn:books">
<a:title>Learning XML</a:title>
</a:book>
<qqq:book xmlns:qqq="urn:books">
<qqq:title>Learning XML</qqq:title>
</qqq:book>Both parse to exactly the same expanded names:
{urn:books}book
{urn:books}title
A processor comparing these two documents on
name has nothing to compare that differs. The
prefix is not part of the name at all.The corollary is the part that costs people afternoons. Because the URI is the name, it is compared as a character string: no URI normalisation, no case folding, no percent-decoding, no trailing-slash equivalence. A pair of URIs a browser would treat as one address can be two namespaces.
| Namespace name | Why it differs from the first row |
|---|---|
| http://example.com/ns | The reference point. |
| http://example.com/ns/ | Trailing slash. No path normalisation happens. |
| HTTP://EXAMPLE.COM/ns | Scheme and host case matter here, although they do not in HTTP itself. |
| http://example.com/ns# | An empty fragment is still a fragment. |
| https://example.com/ns | Different scheme. Upgrading a site to TLS has broken schemas this way. |
#Default namespaces, and the rule about attributes
Writing xmlns="urn:books" on an element sets a default namespace, and every unprefixed element name in its scope now belongs to it. Atom feeds, SVG and most SOAP payloads use this form, because writing a prefix on several hundred elements is miserable. It also means an unprefixed name in a document is very often not in "no namespace" at all, which is the assumption most XPath expressions are built on.
Then comes section 6.2, the most misread clause in the specification: "A default namespace declaration applies to all unprefixed element names within its scope. Default namespace declarations do not apply directly to attribute names." And a sentence later: "the namespace name for an unprefixed attribute name always has no value."
<book xmlns="http://example.com/books" id="b1">
<title>Learning XML</title>
</book>
book -> {http://example.com/books}book
id -> {http://example.com/books}id
title -> {http://example.com/books}title<book xmlns="http://example.com/books" id="b1">
<title>Learning XML</title>
</book>
book -> {http://example.com/books}book
id -> id, in no namespace whatsoever
title -> {http://example.com/books}titleAn unprefixed attribute is never in a namespace. The only way to put an attribute into one is to write the prefix yourself, which is why xml:lang, xsi:type and xlink:href always appear with their prefixes: no other spelling exists. It is also why XML Schema defaults attributeFormDefault to "unqualified" while most authors set elementFormDefault to "qualified". XSD is following the namespace rules, not inventing its own.
The rule cuts the other way too. On top of the plain XML ban on repeating an attribute name, the Recommendation adds a constraint called Attributes Unique: two attributes whose literal names differ are still an error if their expanded names match. An element carrying xmlns:a="u" and xmlns:b="u" may not set both a:x and b:x. That is one attribute written twice.
#Scope, undeclaring, and the two reserved prefixes
Section 6.1 defines scope lexically: "The scope of a namespace declaration declaring a prefix extends from the beginning of the start-tag in which it appears to the end of the corresponding end-tag, excluding the scope of any inner declarations with the same NSAttName part." Two things follow. A declaration applies to the element carrying it, not only to its children, which is what makes xmlns:soap on the soap:Envelope tag legal. And an inner declaration shadows an outer one for the rest of that subtree, silently.
A default namespace can also be switched off. The value of a default declaration may be empty, which the Recommendation says "has the same effect, within the scope of the declaration, of there being no default namespace". The specification's own example is a fragment of XHTML holding an element that deliberately belongs to nothing.
<catalog xmlns="urn:books" xmlns:m="urn:meta">
<book> <!-- {urn:books}book -->
<title>Learning XML</title><!-- {urn:books}title -->
<m:added>2026-01-04</m:added>
<notes xmlns="urn:internal">
<line>Second copy.</line> <!-- {urn:internal}line -->
</notes>
<raw xmlns="">
<p>Not in any namespace.</p> <!-- p, no namespace -->
</raw>
</book>
</catalog>
<!-- From the Recommendation itself, section 6.2: -->
<td><brandName xmlns="">Huntsman</brandName></td>Undeclaring a prefix is where the two versions diverge. Namespaces in XML 1.0 carries a constraint named No Prefix Undeclaring, making xmlns:p="" illegal outright. Namespaces in XML 1.1 (Second Edition, 16 August 2006) removes it: section 6.1 there allows an empty value on a prefix declaration, and 1.1 also widens namespace names from URI references to IRIs. Almost nothing in the wild is XML 1.1, so assume 1.0 rules.
Two prefixes are reserved. xml is permanently bound to http://www.w3.org/XML/1998/namespace and must not be bound to anything else; it needs no declaration, which is why xml:lang, xml:space, xml:base and xml:id work anywhere. xmlns is bound to http://www.w3.org/2000/xmlns/ and must not be declared at all, by anyone. Beyond those, every prefix beginning with the letters x, m and l in any case is reserved for future W3C use.
#The copied fragment, in both directions
Namespace declarations live on ancestors, while the thing people copy is a descendant. Take a fragment out of a namespaced document and the declarations that gave it meaning stay behind on the root element you did not select.
If the fragment used a prefix, the failure is loud: the prefix is unbound, which violates the Prefix Declared constraint and is a fatal namespace error. If the fragment relied on a default namespace, the failure is silent. The elements are still well-formed, they are simply in no namespace now, and every schema check and XPath expression written against them stops matching for a reason that appears nowhere in the file.
<!-- Copied from inside a SOAP envelope. -->
<soap:Body>
<getQuote xmlns="urn:quotes">
<symbol>ACME</symbol>
</getQuote>
</soap:Body>
<!-- soap is bound to nothing. Fatal namespace error. -->
<!-- The reverse: a no-namespace fragment pasted
into a document that has a default namespace. -->
<order xmlns="urn:orders">
<config>
<retries>3</retries>
</config>
</order>
<!-- config and retries are now {urn:orders}config
and {urn:orders}retries. Nothing said so. --><soap:Body xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<getQuote xmlns="urn:quotes">
<symbol>ACME</symbol>
</getQuote>
</soap:Body>
<!-- Reset the default namespace on the pasted root. -->
<order xmlns="urn:orders">
<config xmlns="">
<retries>3</retries>
</config>
</order>The fix either way is to say on the fragment root what the fragment is meant to mean: carry the declarations down with it, or write xmlns="" to state that it belongs to nothing. Both are one attribute. The failure mode is not knowing which one you needed.
#What all this does to XPath
Here is the consequence that catches people who understood everything above. XPath 1.0, the version every browser implements, has no concept of a default namespace: an unprefixed name in an expression means "in no namespace", full stop. MDN puts it flatly: "XPath defines QNames without a prefix to match only elements in the null namespace. There is no way in XPath to pick up the default namespace as applied to a regular element reference."
So against a document whose root carries xmlns="urn:books", //book returns zero nodes. Not an error: zero nodes, which looks exactly like a document containing no book elements, which is why people go and check their data instead of their expression. Bind a prefix in the evaluation context and use it. The prefix is yours to choose: it is a variable in the host language, it never appears in the document, and it need not match anything the document uses.
const src = '<lib xmlns="urn:books"><book id="b1"><title>Learning XML</title></book></lib>';
const doc = new DOMParser().parseFromString(src, 'application/xml');
// 1. The obvious expression. Matches nothing.
doc.evaluate('//book', doc, null,
XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null).snapshotLength; // 0
// 2. Bind a prefix. Any prefix. It is local to this call.
const resolver = (prefix) => (prefix === 'zz' ? 'urn:books' : null);
doc.evaluate('//zz:book', doc, resolver,
XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null).snapshotLength; // 1
// 3. No resolver, at the cost of legibility.
doc.evaluate("//*[namespace-uri()='urn:books' and local-name()='book']",
doc, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null).snapshotLength; // 1Note what stays unprefixed throughout: the attribute. By the section 6.2 rule, id on that book element is in no namespace, so it is selected as @id and never as @zz:id. That mistake is the second most common namespace bug in XPath.
The XPath tester on this site walks the parsed document for every xmlns and xmlns: declaration and builds a resolver from them, so a prefix the document uses simply works. When a document with a default namespace is queried by an expression containing no prefix, it says so before showing an empty result. XPath 2.0 and later do have a default element namespace, which explains an expression that works in Saxon and returns nothing in a browser.
Common questions
Does the namespace URI have to be a real URL that resolves?
No. It is an identifier, and no conforming XML parser fetches it. The Namespaces Recommendation requires only that it be a URI reference, which is why urn:isbn:0-596-00420-6, tag:example.com,2011:feed and http://example.com/a-path-that-404s are all valid namespace names.
The reason http URIs are used so widely is ownership rather than retrieval: a domain you control gives you a collision-free space to name things in. Serving documentation at that address is a nice thing to do and has no technical effect. If something in your build is making network requests for namespace URIs, look at xsi:schemaLocation or a DTD reference, not at xmlns.
Why does my XPath return nothing when the element is obviously there?
Almost certainly because the document declares a default namespace and your expression does not use a prefix. In XPath 1.0 an unprefixed name matches only elements in no namespace, so //book cannot match {urn:books}book no matter how the document is written.
Bind a prefix to the namespace URI in whatever your host language calls a namespace resolver, then write //p:book. The prefix is entirely yours to pick. If registering one is awkward, //*[local-name()="book"] works everywhere at the cost of also matching a book element from any other vocabulary in the file.
Can I just change the prefix in a document without breaking anything?
Yes, as long as you change the declaration to match and you change every use of it within that declaration's scope. The prefix carries no meaning of its own, so renaming soap: to env: throughout a document produces a document that is equivalent in every way a processor can observe.
Two caveats. Prefixes can appear inside attribute values and inside element text, in QName-valued content such as xsi:type="tns:AddressType" or an XSLT match pattern, and a naive find and replace across the file will miss them or break them. And code that compares nodeName or tagName rather than localName plus namespaceURI will notice the change, which is a bug in that code but still a bug you now own.
Is xmlns an attribute? It looks like one.
It is written as one and it is an attribute in the XML 1.0 sense, but a namespace-aware processor consumes it. Namespaces in XML 1.0 section 3 says namespace declaration attributes are not treated as ordinary attributes: they are removed from the attribute list a namespace-aware application sees, and the DOM exposes them through separate namespace machinery rather than through getAttribute in the normal way.
The practical version is that you cannot declare xmlns in a DTD or an XSD as though it were content, you should not iterate over an element's attributes and expect to find or not find it consistently across parsers, and setting it with setAttribute rather than setAttributeNS produces a node that serialises correctly and behaves oddly.