XML

Matches

Expressions worth knowing

  • every sim element, at any depth
  • an attribute rather than an element
  • filter on an attribute, then step down
  • filter on a child element value
  • the first sim in the document, not the first per parent
  • a number, not a node set
  • match by name, ignoring namespaces entirely
  • no prefix — the trap. Click it and read why nothing matched

Why //Body finds nothing in a SOAP response

Start with the problem that brings most people here. You have a SOAP response, you write //Body, and you get nothing. So you try /Envelope/Body, then you start doubting the document, then you paste it into a different tool and get nothing there either. The expression is fine. The document is fine.

The cause is that XPath 1.0 has no concept of a default namespace. An unprefixed name in an expression means "an element in no namespace at all", and every element in a SOAP document is in one. So //Body is not a near miss — it is asking a question with no answer. You need //soap:Body with the prefix bound, or the escape hatch //*[local-name()='Body'].

Most testers respond to this with an empty results box, which reads as "your expression is wrong" and sends you off rewriting something that was already correct. This page reads the namespaces out of your document, lists them above the results with their prefixes, and when an unprefixed expression comes back empty against a namespaced document it says so in as many words. A default namespace declared as xmlns="…" has no prefix to copy, so one is invented and shown to you — that is not a workaround for a bug here, it is what every XPath 1.0 host has to do.

Everything runs on your machine using the browser's own XPath engine, the same one behind document.evaluate. Nothing is uploaded, which matters given how often the XML people debug is a real response from a real system.

How to use it

  1. Paste your XML – A SOAP response, a config file, an RSS feed, an export — anything well-formed. A parse error is reported with the line rather than swallowed.
  2. Look at the namespace strip – Before writing anything, read what prefixes the document actually offers. This is the step that saves the afternoon.
  3. Type an expression – Results update as you type. The examples under the panels are clickable if you want a starting point.
  4. Turn on "show paths" when values repeat – Six matches all reading "up" tell you nothing. The paths tell you which six.
  5. Read what came back – A node set lists each match. Functions like count() and string() return a single value, and are labelled as such rather than shown as a one-item list.

When an expression fights you, cut it back to /* and add one step at a time. The step where the match count drops to zero is the step that is wrong, and it is almost never the one you suspected. If the count drops on the very first step, you are looking at the namespace problem. To go the other way and search JSON instead, the JSONPath Tester is the same idea for a different format.

The same document, four expressions

A SIM inventory export with a default namespace on the root, which is the shape that causes the trouble. The prefix d1 below is one this page invented, because the document declares its namespace without a prefix and XPath cannot address it otherwise.

SIM inventory exportXPath 1.0
sims.xmldefault namespace
<inventory xmlns="urn:telecom:sim-inventory"
           xmlns:net="urn:telecom:network">
  <sim iccid="8944501012345678901" roaming="true">
    <msisdn>447700900112</msisdn>
    <status>active</status>
    <net:cell mcc="234">
      <net:rsrp>-97</net:rsrp>
    </net:cell>
  </sim>
  … two more sim elements …
</inventory>
expressionsand what they return
# the mistake everyone makes first
//sim
→ nothing. sim is in the default namespace.

# the same query, with the bound prefix
//d1:sim[@roaming='true']/d1:msisdn
→ "447700900112", "447700900204"

# namespaces sidestepped entirely
//*[local-name()='rsrp']
→ "-97", "-103", "-112"

# a number, not a node set
count(//net:cell)
→ 3

When you reach for this

Working out why an integration reads the wrong field

A pipeline maps XML into something else with a handful of XPath expressions, and one of them silently returns empty. Paste a real payload here, run the expression, and you find out in seconds whether it is the expression, the namespace or genuinely missing data. Those three look identical from inside the pipeline's logs and need completely different fixes.

Writing a selector before you write the code

Getting an expression right in a browser is faster than getting it right through a compile-and-run loop. The same expressions work in XSLT, in Java's javax.xml.xpath, in Python's lxml and in most XML libraries, so what you settle on here transfers directly.

Checking a document has what you were promised

count(//sim) against a supplier's file answers "did we get all 4,000 records" before anything downstream has a chance to half-import them. If you would rather see the whole file as a grid first, XML to Table does that, and XML Validator tells you whether it is well-formed at all.

What it handles

  • Namespaces read straight out of your document, with each prefix listed before you start typing.
  • A prefix invented and bound for a default xmlns="…" declaration, which is otherwise unaddressable in XPath 1.0.
  • A specific explanation when an unprefixed expression comes back empty against a namespaced document, instead of a blank box.
  • A valid expression that matches nothing kept visibly separate from an expression that could not be parsed.
  • The full path of every match, so repeated values can be told apart.
  • Functions returning a number, a string or a boolean — count(), string(), contains() — labelled as values rather than dressed up as a one-item node set.
  • Attributes, text nodes, comments and CDATA sections all reported with what they are.

Questions worth answering

Why does //Body match nothing when I can see Body in the document?

Because the element you can see is soap:Body, or it sits under a default xmlns, and either way it is in a namespace. In XPath 1.0 an unprefixed name matches only elements in no namespace, so the expression is asking for something that is not there. Use the prefix shown in the namespace strip above the results, or //*[local-name()='Body'] if you would rather not think about namespaces at all. It is the single most common XPath frustration and it has nothing to do with your document being malformed.

Which version of XPath does this support?

1.0, because that is what browsers implement and this runs in yours. In practice that covers the great majority of expressions people write. What you will miss are the 2.0 and 3.1 string functions — matches(), ends-with(), replace() — along with sequences and for expressions. If one of those returns an "invalid expression" error here, it is not a typo; the function does not exist in 1.0. contains(), starts-with() and substring() all do.

Why does (//sim)[1] give a different answer from //sim[1]?

This trips up nearly everyone at least once. //sim[1] reads as "every sim that is the first sim within its own parent", so a document with five parents gives you five matches. (//sim)[1] collects every sim in the document first, then takes the first of those — one match. The brackets change what the predicate applies to, not merely the order of evaluation.

Can I use it on HTML?

The document is parsed as XML, so HTML only works if it happens to be well-formed XML too, which most real HTML is not — an unclosed <br> or a bare & is enough to stop it. For pulling data out of an HTML page, HTML Table to JSON is the tool that expects it, and CSS selectors are the more natural language for HTML anyway.

What does "show paths" actually give me?

A location path for each match, like /inventory[1]/sim[3]/status[1], with the positional index at every step. When eleven matches all read active, the values are useless and the paths are the entire answer — that is how you find which record is the one you care about. It is also a fast way to sanity-check an expression you are unsure of: if the paths are not where you expected, the expression is matching more than you thought.

Does the document leave my browser?

No. Parsing and evaluation both use APIs built into the browser, and nothing is sent anywhere. That is deliberate: the XML worth testing an expression against is usually a real response from a real system, and pasting one of those into a service that uploads it is a different kind of problem.

Related tools

Further reading

  • Namespaces in XML (W3C) – The specification behind the prefix rules, and why a default namespace behaves differently.
  • XPath on MDN – Function-by-function reference for the 1.0 engine your browser ships.