XPath Tester

Evaluate XPath and see every match, with the namespace traps explained.

Input
WaitingPaste a document to check it. Validation runs as you type.

Everything runs in this tab. Nothing you paste is uploaded, logged or sent anywhere. Open your network panel and check.

Paste a document, type an expression, and every matching node is listed with its type, the path that reaches it (/catalog/book[2]/title) and its value, with the match count and the evaluation time above the list. Evaluation runs on the browser's own engine through document.evaluate, so this page ships no XPath library and nothing you paste leaves the tab.

You reach for it when an expression is about to go somewhere awkward to debug: a Schematron rule, an XSLT match pattern, a Camel splitter, a scraper selector. Finding out here that //book selects nothing is cheaper than finding out from a silent empty result in production.

What is different is the namespace handling. Most free testers hand document.evaluate a null resolver, so any prefixed expression throws and any document with a default namespace returns zero matches with no explanation. This one collects every xmlns declaration in the document, binds them all, and names the default-namespace trap before it hands back an empty result.

The expression that matches nothing

This is the most common XPath failure, and its symptom is indistinguishable from the element genuinely not being there. Take a document whose root carries xmlns="urn:books". The element inside it is not named book; its expanded name is {urn:books}book. XPath 1.0 has no concept of a default namespace, so an unprefixed name means "in no namespace", and //book asks for {}book. There is no such node. Zero matches, no error.

MDN puts it flatly: there is no way in XPath to pick up the default namespace as applied to a regular element reference. Bind a prefix of your own to the URI instead. The prefix is local to the expression, so if the document says xmlns:b="urn:books" you may still write //x:book, provided you bind x.

When this page finds a default namespace it binds the prefix ns to it, so //ns:book works with no setup, and it prints the trap above the result with your actual URI in it. The namespace box takes your own bindings as prefix=uri pairs, and those override what was harvested from the document.

  • Bind a prefix: //ns:book/ns:title. Shortest, and what you want in code.
  • Ignore namespaces: //*[local-name()="book"]. Matches a book in any namespace, and worth knowing when you cannot control the resolver.
  • Be exact without a prefix: //*[namespace-uri()="urn:books" and local-name()="book"].
  • Attributes differ. A default namespace never applies to attribute names, so in <book xmlns="urn:books" id="7"/> the element is {urn:books}book but the attribute is plain {}id. Select it as @id, not @ns:id.

Slashes, predicates and positions

/ is a child step. // is shorthand for /descendant-or-self::node()/, which is why /catalog/book finds only book elements directly under the root while //book finds them at any depth. The second is more forgiving and considerably slower on a large document, because it visits every node rather than one node's children.

Predicate indexes start at 1, not 0, so an expression ending in [0] silently returns nothing. The subtler trap is that a predicate binds to its step rather than the whole expression: //book[1] means "every book that is the first book child of its own parent", so a document with three catalogs returns three nodes. For the first match overall you need parentheses, (//book)[1].

position() and last() are functions of the context, the node list the current step produced: book[last()] is the final book under each parent. A bare number is shorthand for [position() = 2], which is why //book[@lang="en"][1] and //book[1][@lang="en"] are different sets. The first filters then takes one; the second takes one then filters.

  • child:: is the default axis, so book and child::book are the same expression.
  • descendant:: searches downward; parent:: (..) and ancestor:: search upward.
  • following-sibling:: and preceding-sibling:: stay at one level, which is how you say "the price after this title".
  • attribute:: is written @, self:: is written . in an abbreviated expression.
  • namespace:: is in the spec but Firefox does not implement it. Do not build on it.

What XPath 1.0 does not have

Browsers implement XPath 1.0 and nothing else. The API came from DOM Level 3 XPath, now a retired W3C Note, and lives on in section 8 of the WHATWG DOM Standard. No browser has 2.0 and none is coming, so this page states the ceiling rather than advertising a version it cannot deliver.

XPath 1.0 has four types: node-set, string, number, boolean. 2.0 replaced that model with sequences and schema-aware typing and brought what people miss most: matches(), replace() and tokenize(), real date types, for and if expressions. 3.1 added maps, arrays and the => arrow. All of it fails here, and equally in PHP's DOMXPath, .NET's XPathNavigator and Java's javax.xml.xpath, which are 1.0 too. The workarounds you will use most are substring-before and substring-after in place of tokenize, and translate() in place of a character class.

Reading the result

Not every expression returns nodes. count(//book) returns a number and string(/catalog/@id) returns a string, so the panel says which of the four types came back and prints scalars as values rather than as an empty list. A scalar 0 and an empty node-set look alike in most tools and mean different things.

Each match shows its node type (element, attribute, text, cdata, comment, processing-instruction), the path that reaches it, and its value: serialised XML for an element, the attribute value for an attribute. Matched content is written into the page as text and never as markup, so a document containing a script element cannot execute anything. The document must be well-formed before an expression runs, an unbound prefix is reported by name alongside the prefixes that are available, and the first 1,000 matches are rendered while the count above the list is the true total.

Doing this in code

All six of these implement XPath 1.0, and all six make you register namespace prefixes yourself. The document's own prefixes are never picked up automatically, which is why the namespace argument appears in every sample.

const source = `<?xml version="1.0"?>
<library xmlns="urn:books">
  <book id="b1"><title>XML in a Nutshell</title></book>
</library>`;

const doc = new DOMParser().parseFromString(source, 'application/xml');
if (doc.querySelector('parsererror')) throw new Error('not well-formed');

// document.createNSResolver is deprecated: it now returns its input
// unchanged and is kept only for compatibility. Write the resolver
// yourself. These prefixes are local to the expression and need not
// match the ones the document uses.
const NS = { bk: 'urn:books' };
const resolve = (prefix) => NS[prefix] ?? null;

// Snapshot, not iterator: an iterator result is invalidated by any
// mutation of the document while you are still walking it.
const snap = doc.evaluate(
  '//bk:book[@id="b1"]/bk:title',   // //title would match nothing
  doc,
  resolve,
  XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
  null,
);

for (let i = 0; i < snap.snapshotLength; i++) {
  console.log(snap.snapshotItem(i).textContent);
}
from lxml import etree

# lxml's defaults resolve entities and fetch external DTDs. All three
# flags below are needed to close that off.
parser = etree.XMLParser(resolve_entities=False, no_network=True, load_dtd=False)
tree = etree.fromstring(source.encode('utf-8'), parser)

# lxml refuses a None key in the namespaces map, for the same reason the
# spec does: XPath 1.0 cannot address a default namespace. Give it a prefix.
ns = {'bk': 'urn:books'}

for title in tree.xpath('//bk:book/bk:title', namespaces=ns):
    print(title.text)

# An invalid expression raises rather than returning empty.
try:
    tree.xpath('//bk:book[')
except etree.XPathEvalError as e:
    print(f'bad expression: {e}')

# The escape hatch when you cannot register prefixes:
tree.xpath("//*[local-name()='book']")
import javax.xml.XMLConstants;
import javax.xml.namespace.NamespaceContext;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.xpath.*;
import org.w3c.dom.NodeList;
import java.util.Iterator;
import java.util.Map;

DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();

// setNamespaceAware is FALSE by default. Leave it off and every prefixed
// element is treated as one long local name, so bk:book never matches.
// This is the usual cause of "it works in xmllint but not in Java".
dbf.setNamespaceAware(true);
dbf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);

var doc = dbf.newDocumentBuilder()
    .parse(new java.io.ByteArrayInputStream(bytes));

XPathFactory xpf = XPathFactory.newInstance();
xpf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
XPath xpath = xpf.newXPath();

Map<String, String> ns = Map.of("bk", "urn:books");
xpath.setNamespaceContext(new NamespaceContext() {
    public String getNamespaceURI(String prefix) {
        return ns.getOrDefault(prefix, XMLConstants.NULL_NS_URI);
    }
    public String getPrefix(String uri) { return null; }
    public Iterator<String> getPrefixes(String uri) { return null; }
});

NodeList nodes = (NodeList) xpath.evaluate(
    "//bk:book/bk:title", doc, XPathConstants.NODESET);

for (int i = 0; i < nodes.getLength(); i++) {
    System.out.println(nodes.item(i).getTextContent());
}
using System.Xml;
using System.Xml.XPath;

var settings = new XmlReaderSettings
{
    DtdProcessing = DtdProcessing.Prohibit,
    XmlResolver = null,
};

using var reader = XmlReader.Create(new StringReader(source), settings);
var nav = new XPathDocument(reader).CreateNavigator();

// XmlNamespaceManager is not optional. .NET has no default-namespace
// concept in XPath either, so bind a prefix and use it.
var ns = new XmlNamespaceManager(nav.NameTable);
ns.AddNamespace("bk", "urn:books");

foreach (XPathNavigator node in nav.Select("//bk:book/bk:title", ns))
{
    Console.WriteLine(node.Value);
}

// Compile once if the expression is reused: Select() reparses every call.
XPathExpression expr = nav.Compile("count(//bk:book)");
expr.SetContext(ns);
Console.WriteLine((double)nav.Evaluate(expr));
<?php
$doc = new DOMDocument();
// LIBXML_NONET stops libxml2 fetching anything the document references.
if (!$doc->loadXML($source, LIBXML_NONET)) {
    fwrite(STDERR, "not well-formed\n");
    exit(1);
}

$xpath = new DOMXPath($doc);

// Prefixes must be registered even when the document declares them.
// registerNamespace('', ...) is accepted but useless: XPath 1.0 still
// cannot address the empty prefix.
$xpath->registerNamespace('bk', 'urn:books');

foreach ($xpath->query('//bk:book/bk:title') as $node) {
    echo $node->textContent, "\n";
}

// query() returns false on an invalid expression, not an empty list.
// A loose == comparison would read that false as "no results".
$result = $xpath->query('//bk:book[');
if ($result === false) {
    fwrite(STDERR, "invalid XPath expression\n");
}
# xmllint ships with libxml2 and is almost certainly already installed.
# --xpath has no way to register a namespace prefix, so against a
# namespaced document it prints "XPath set is empty" and exits 10.
xmllint --nonet --xpath '//book/title' doc.xml

# The interactive shell does have setns, and reads fine from a heredoc:
xmllint --nonet --shell doc.xml <<'EOF'
setns bk=urn:books
xpath //bk:book/bk:title
EOF

# Or sidestep prefixes entirely:
xmllint --nonet --xpath "//*[local-name()='title']/text()" doc.xml

# xmlstarlet is the friendlier option if you can install it:
xmlstarlet sel -N bk=urn:books -t -v '//bk:book/bk:title' -n doc.xml

Two things repeat across all six. Namespace prefixes are yours to declare rather than the document's to supply, and an invalid expression is signalled differently in each (a thrown DOMException, a raised XPathEvalError, a returned false, an exit status of 10), which is why none of these calls the query and uses its result on one line.

Common questions

Why does my XPath return no results?

Usually because the document has a default namespace and your expression has no prefix. If the root carries xmlns="urn:something", book inside it is really {urn:something}book, and //book asks for a book in no namespace. Zero matches and no error, because from XPath's point of view nothing went wrong.

This page detects that, names the URI it found, and binds the prefix ns to it so //ns:book works straight away. If you would rather avoid prefixes, //*[local-name()="book"] matches on the local name regardless of namespace. The other causes to rule out are case, since XPath is case sensitive, and a predicate of [0] when indexes start at 1.

Is my XML uploaded when I test an expression?

No. The well-formedness scan runs in a Web Worker in this tab and the expression is evaluated by document.evaluate, a local browser API. There is no server component here to send anything to.

That matters more for XPath than for most tools, because the documents people write expressions against are real payloads rather than samples: a response captured from a partner API, a message off a queue, a SAML assertion. Open the Network tab, paste a document, run an expression, and watch it stay empty.

Which version of XPath does this support?

XPath 1.0, because that is what browsers implement and there is no alternative. Evaluation runs on document.evaluate, defined in section 8 of the WHATWG DOM Standard. Chrome, Firefox and Safari are all 1.0 and none has announced any intention to go further.

So matches(), replace(), tokenize(), for and if expressions, date types, sequences, maps and arrays all fail here, as they do in PHP's DOMXPath, .NET's XPathNavigator and Java's javax.xml.xpath. If you need 2.0 or 3.1, that means Saxon: Saxon-JS in a browser, Saxon-HE on the JVM or .NET.

What is the difference between / and // in XPath?

/ selects a direct child; // is shorthand for /descendant-or-self::node()/ and selects at any depth. So /catalog/book matches book elements immediately inside the root catalog, while //book matches book anywhere. A leading / anchors at the document root, which is why /book fails on a document whose root is catalog.

The trap is combining // with a predicate. //book[1] does not mean "the first book in the document"; the predicate applies to the step, so it means "every book that is the first book child of its parent", and three catalogs give you three nodes. Wrap it to get what you meant: (//book)[1].

How do I select an attribute rather than an element?

Put @ before the name. //book/@id selects the id attribute node and the panel shows its value, its owning path and its type. To filter by an attribute instead of selecting it, put it in a predicate: //book[@id="b1"] selects book elements, not attributes.

Attributes have their own namespace rule, and it is the one people get wrong. A default namespace declaration never applies to attribute names, so in <book xmlns="urn:books" id="7"/> the attribute is plain {}id: select it as @id, because @ns:id matches nothing. An attribute is namespaced only when you write the prefix yourself, as in xlink:href or xsi:schemaLocation.

Can I test XPath against HTML here?

Only if the HTML is well-formed XML, which most is not. Input is parsed as application/xml, so unclosed br tags, unquoted attribute values and raw ampersands in URLs are rejected before any expression runs. That is deliberate: XPath behaves differently against an HTML DOM, where element names are lower-cased and everything sits in the XHTML namespace.

Against a strict XMLDocument, as here, node tests are case sensitive and namespaces behave as specified. For real-world HTML use a forgiving parser instead: lxml.html in Python, jsoup in Java, or querySelector when a CSS selector will do. XHTML, SVG and a fragment you have already tidied all parse here.

Related tools

Background reading

Errors this fixes