XPath Tester
Evaluate an XPath 1.0 expression against your XML and see every matched node with its line and path — namespace prefixes bound explicitly, on this device.
Evaluated live against the XML below — XPath 1.0, on this device.
Results appear here once you provide XML and an XPath expression.
Your XML appears here with every matched node highlighted once an expression matches.
Press ? for shortcuts
Do it offline:$ xmllint --xpath '//book[@id="1"]/title/text()' catalog.xml
How to use it
- Paste or drop your XML into the document pane; it is parsed on this device and never uploaded.
- Type an expression in the field above it — evaluation is live, so there is no Run button to press.
- If the document uses namespaces, add a prefix and its URI in the binding table, then use that prefix in the expression.
- Read the result: a node-set lists each match as path, line and string-value; a number, string or boolean is shown as a single typed line.
Paste XML and an XPath expression; every matched node comes back with its path, its line and its string-value. This tester implements XPath 1.0 as the W3C Recommendation defines it — thirteen axes, predicates and the core function library — and namespace prefixes are bound in a table you control. An expression that matches nothing is reported as a result, and nothing you paste leaves this tab.
How it works
An expression is a sequence of steps separated by /, and each step names an axis, a node test and any number of predicates. The axis is the direction of travel from the current node: child, descendant, parent, ancestor, following-sibling, preceding-sibling, following, preceding, attribute, namespace, self, descendant-or-self and ancestor-or-self — thirteen in all. Because writing child:: in front of every step would be tiresome, the Recommendation defines abbreviations: a bare name means child::, @ means attribute::, // means /descendant-or-self::node()/, . means self::node() and .. means parent::node(). So //book/@id is the long form descendant-or-self::node()/child::book/attribute::id. A leading / is the document node, not the root element, which is why /* selects the outermost element and .. from there selects nothing.
A predicate in square brackets filters the nodes a step produced, and it is evaluated once per node with that node as context. A number stands for position(), so book[2] is the second book; last() is the final one. Position is counted along the axis, not through the document, and that has two consequences worth internalising. On a reverse axis such as preceding-sibling, [1] is the nearest node going backwards, not the earliest one in the file. And because // expands to a descendant step, //title[1] asks for every title that is the first title of its own parent, which can be many nodes; wrapping the path first, as (//title)[1], is what takes a single node. Comparing a node-set is existential: @lang != 'en' holds when some selected attribute differs, so it is not the negation of @lang = 'en'.
Namespaces are where XPath 1.0 surprises people, and the rule is exact: an unprefixed name test matches only names in no namespace. A default declaration, xmlns="…", puts the elements it covers into a namespace without giving them a prefix, so //title over such a document matches nothing at all — correctly, and silently, unless something says so. The expression language has no notion of the document's default; a prefix must be bound outside the expression by the host, which here is the binding table. Bind c to the document's URI, write //c:title, and the match returns. Prefixes need not be spelled the way the document spells them, because only the URI is compared; two documents that disagree on prefix spelling are queried by the same expression once each is bound to the same URI.
Examples
//book[@id='1']/title/text()/catalog/book[1]/title/text() · line 3 → XML in PracticeThe predicate keeps the book whose id attribute is 1, then the path descends to its title and finally to the text node inside it. The left column is the positional path of the match, the middle is the line it starts on, and the right is its string-value.
//book/@id/catalog/book[1]/@id · line 2 → 1
/catalog/book[2]/@id · line 6 → 2
/catalog/book[3]/@id · line 10 → 3Attributes are not children of their element, so a plain step never reaches them; @ is the abbreviation for the attribute axis. Each match here is an attribute node in its own right, with the line of the element that carries it.
//book[2]/preceding-sibling::book[1]/title/catalog/book[1]/title · line 3 → XML in Practicepreceding-sibling is a reverse axis, so its [1] is the sibling immediately before the second book rather than the first book in the file. Reading it as document order is the usual way to pick the wrong node on a long list.
//title[1]/catalog/book[1]/title · line 3 → XML in Practice
/catalog/book[2]/title · line 7 → Namespaces Explained
/catalog/book[3]/title · line 11 → XPath RezepteThree matches, not one: // is a descendant step, and the predicate applies to each parent's own title children, all of which are firsts. Write (//title)[1] when you want the single earliest title, since the brackets then filter the whole gathered set.
count(//book[price < 25])2Not every expression returns nodes. This one returns the number 2, shown as a single typed line. The comparison inside the predicate converts each price element to a number through its string-value, so the two books under 25 survive and the 39.50 one does not.
//titleThis expression is valid and matched no nodes.This document puts everything in a default namespace, and an unprefixed name test can only match names in no namespace, so the answer is an empty node-set. It is a correct answer to the question asked; the tool says which question you actually asked.
//c:book/c:title/text()/catalog/book/title/text() · line 2 → XML in PracticeThe same document with c bound to http://example.com/catalog in the table. The prefix is a local nickname for the URI and need not be the one the file uses, which is how a single expression can be run against documents that spell their prefixes differently.
Every example on this page is executed against the same engine that powers the tool, on every build.
Gotchas worth knowing
The document node and the root element are different things
A leading / addresses the document node, whose only element child is the outermost element, so /* is how you name that element without knowing what it is called. Comments and processing instructions outside it are children of the document node too, and .. from there selects nothing at all.
Adjacent text and CDATA collapse into one text node
In the XPath data model a text node never sits next to another text node, so a run like t1<![CDATA[<raw>]]>t2 is a single text node whose string-value is t1<raw>t2. count(//text()) reports 1 for it. Whitespace-only text between elements is a real text node as well, and it is counted like any other.
Namespace declarations never appear on the attribute axis
An xmlns or xmlns:prefix declaration is not an attribute in the data model, so //@* will not return it. Those declarations surface on the namespace axis instead, where each in-scope binding — including the inherited xml prefix — is one node whose string-value is the URI.
XPath's own number grammar is narrower than JavaScript's
number("1e3") is NaN here, and so are number("0x10") and number("+1"), because the Recommendation's number production allows only an optional minus, digits and a decimal point. round(-0.5) is negative zero, which prints as 0, and substring("12345", 1.5, 2.6) is "234" after both arguments are rounded.
A hyphen is part of a name unless you put spaces round it
a-b is one element name, not a minus b, so subtraction needs whitespace: a - b. For the same reason division and remainder are spelled as the words div and mod rather than / and %, since / was already taken by the step separator.
Comparing node-sets asks whether any pair matches
//book/@lang != 'en' is true when at least one selected attribute is not en, while not(//book/@lang = 'en') is false because some attribute is en. The two are different questions, and the second is usually the one you meant when a filter behaves backwards.
Large documents and long expressions are refused, not truncated
Evaluation happens in this tab, so a document over 5 MB is declined whole rather than partly parsed, and an expression over 4 KB is refused the same way. A node-set is listed up to 10,000 matches; past that the tool says a real match went unlisted rather than presenting a short list as complete.
Common errors
Unexpected end of expression at position 8 — a node test was expected — a name, '*', or node()/text()/comment()/processing-instruction().- A step was opened and then nothing followed it — here a predicate bracket with no content. The position is the character the parser stopped at, and the caret under the expression points at it.
Unknown axis 'foo' at position 3 — XPath 1.0 has 13: child, descendant, parent, ancestor, following-sibling, preceding-sibling, following, preceding, attribute, namespace, self, descendant-or-self, ancestor-or-self.- Whatever precedes :: must be one of the thirteen axis names, and the message lists them so the correction is in front of you. A typo like preceeding-sibling lands here rather than quietly matching nothing.
Unbound namespace prefix 'm' at position 3 — add a prefix → URI row so the expression can be resolved.- A prefix in an expression is resolved by the host, not by the document, so writing m: is not enough on its own. Add a row binding m to the URI the document uses and the same expression resolves.
XPath 2.0/3.1 construct 'for … in … return' at position 1 — this tool evaluates XPath 1.0 only.- Later versions of XPath added for, if, let, some/every, instance of and the operators !, =>, || and eq/ne/lt/gt/le/ge. This tool evaluates 1.0, so it names the construct it found instead of reporting a syntax error that would send you looking for a typo.
Unexpected end of input at line 1, column 12 — '<a>' opened at line 1, column 1 is never closed; did you forget '</a>'?- The XML itself is not well-formed, so there is no tree to query yet. The parser's own verdict is shown word for word, with the line and column of the tag that was left open, and the expression is not run until the document is repaired.
This XML is larger than the 5 MB limit for this tool. For a bigger document, query it from the CLI: xmllint --xpath '//book[@id="1"]/title/text()' catalog.xml- Parsing and evaluation both run in this tab, so the document is capped at 5 MB. Past that the whole input is declined rather than half-read, and the banner hands you a command-line equivalent that carries no such ceiling.
FAQ
Which version of XPath does this tool evaluate?
XPath 1.0, the W3C Recommendation of 16 November 1999, and only that version. Constructs introduced in 2.0 and 3.1 are recognised and named as out of scope rather than reported as typos, so you learn immediately that the expression needs a different processor.
Why does my expression match nothing when the element is clearly there?
Almost always a namespace. If the document carries xmlns on an ancestor, its elements have a namespace and an unprefixed name test cannot see them. Bind a prefix to that URI in the table, put the prefix on each element name, and the same path starts matching.
Do I have to use the same prefix the document uses?
No. Prefixes are local to the expression and only the URI is compared, so binding x to a URI lets you write //x:name even if the file spells that namespace ns2. It also means a file's own prefix means nothing here until you have bound it yourself.
How do I select an attribute rather than an element?
Use the attribute axis: //book/@id, or attribute::id in full. Attributes are not children in the data model, so no ordinary step will find them, and a namespace declaration is not an attribute at all — it appears only on the namespace axis.
What is the difference between //item[1] and (//item)[1]?
The first is a predicate on a descendant step, so it keeps every item that is the first item of its own parent and can return many nodes. The second filters the completed node-set and returns at most one node, the earliest in document order.
Can I test an expression that returns a number or a boolean?
Yes. count(), sum(), string-length(), boolean(), not() and the rest of the core function library all work, and the result is shown as a single typed line with its type named. Numbers are printed the way XPath's own string() prints them, without exponents.
Is the XML I paste sent to a server?
No. The document is parsed and the expression evaluated in this browser tab, and a file dropped onto the page is read the same local way. Configuration and exports frequently contain hostnames and credentials, so keeping the whole exchange on your device is the safe default.
Keyboard shortcuts
- ⌘/Ctrl + Enter
- Evaluate the current expression again.
- ⌘/Ctrl + S
- Download the result as a .json file.
- ⌘/Ctrl + C
- With nothing selected, copy the result.
- ⌘/Ctrl + Shift + C
- Copy the result even with a selection active.
- t
- Switch to the next colour theme.
- ? or ⌘/Ctrl + /
- Open the keyboard-shortcut help.
- Esc
- Close the open overlay.