In REST Assured, how do you read an XML element and its attributes with GPath?
answer
- dots walk down, @ reads across
- it is Groovy GPath, not XPath
- text() for content, size() for count
- ** or depthFirst() searches any depth
- getNode returns a Node, not a String
basics
~20 sREST Assured evaluates a dotted Groovy GPath over the parsed XML tree: each dot is a child-element step, an attribute step carries @, and text() returns character data. Indexes, size() and closures work; a missing attribute reads as null.
solid answer
~50 sREST Assured parses an XML response with Groovy's `XmlSlurper` and exposes it as a **GPath** tree, so `then().body(path, matcher)` and an `XmlPath` instance speak the same expression language — dots, not slashes. Each dot walks one child element down: `sprayDiary.block.application.product`. An attribute is read with `@`, as in `sprayDiary.block[0].@name`. `text()` returns an element's character data, `size()` counts a node set, and `[0]`, `[-1]` and `[0..1]` index it. Because the path is compiled as Groovy you can filter inline — `sprayDiary.block.find { it.@variety == 'Gala' }.@name` — and `**` (or `depthFirst()`) searches at any depth. `XmlPath` adds typed reads: `getString`, `getInt`, `getList(path, Class)`, plus `getNode(path)` and `getNodeChildren(path)`, which hand back REST Assured's own `Node` and `NodeChildren` with `getAttribute(...)`, `children()`, `name()` and `value()`. An attribute name that is not a legal Groovy identifier has to be quoted — `.'@applied-on'` — and `setRootPath(...)` anchors every later expression under a fixed prefix.
code
java · 15 linesimport static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasItems;
given()
.accept("application/xml")
.when()
.get("/blocks/spray-diary")
.then()
.statusCode(200)
.body("sprayDiary.block.size()", equalTo(2))
.body("sprayDiary.block[0].@name", equalTo("Northfield"))
.body("sprayDiary.block.application.product", hasItems("captan", "sulphur"))
.body("sprayDiary.block[1].application.rate.@unit", equalTo("kg/ha"))
.body("**.find { it.@variety == 'Gala' }.@name", equalTo("Longacre"));go deeper
Be ready to write a dotted path against a small XML body on a whiteboard, and to say out loud that the @ prefix is what makes a step read an attribute instead of a child element.
Explain that the expression is Groovy GPath over an XmlSlurper tree, not XPath, and show why that makes find, findAll and ** available inside the path string.
Show judgment about which reads belong in body(...) matchers and which belong on a held XmlPath, and flag path strings built from caller-supplied values as an injection hazard.
Own the convention: a house style for XML assertions that keeps paths literal, keeps deep ** searches rare enough to stay readable, and survives a schema change without a suite-wide rewrite.
## GPath, not XPath REST Assured runs no XPath engine behind `then().body(...)`. When a response parses as XML the bytes go through Groovy's `XmlSlurper`, and the node tree that comes back is navigated with **GPath** — Groovy's property-style path language. The same expression string works from a `body(path, matcher)` assertion and from an `XmlPath` object, because both evaluate against that one slurped tree. So an orchard spray-diary assertion reads `sprayDiary.block.application`, never `/sprayDiary/block/application`. Assume `GET /blocks/spray-diary` returns: ```xml <sprayDiary season="2026"> <block name="Northfield" variety="Bramley"> <application appliedOn="2026-04-12"> <product>captan</product> <rate unit="kg/ha">1.8</rate> </application> </block> <block name="Longacre" variety="Gala"> <application appliedOn="2026-04-14"> <product>sulphur</product> <rate unit="kg/ha">3.0</rate> </application> </block> </sprayDiary> ``` ## The four moves you need - **Dots step down.** Every dot is a child-element step from the root element, so `sprayDiary.block.application.product` walks four levels and yields both product names. - **`@` reads across.** An attribute is a step prefixed with `@`: `sprayDiary.block[0].@name` is `Northfield`, and `sprayDiary.block.@variety` is the list of both varieties. - **`text()` takes content.** On a single element it is that element's character data; on a node set it is the **concatenation** of every match with no separator — a genuine surprise the first time you hit it. - **`size()` counts.** `sprayDiary.block.size()` is `2`, and `[0]`, `[-1]` and `[0..1]` index and slice the same node set. Two mechanical footnotes. A plain element step already exposes its text to a matcher, so `body("sprayDiary.block.application.product", hasItem("captan"))` needs no `text()` at all. And an attribute whose name is not a legal Groovy identifier must be quoted — `.'@applied-on'`, not `.@applied-on`. ## Filtering, because the path really is Groovy The expression is compiled and executed as Groovy, so closures are part of the language rather than a bolted-on predicate syntax: 1. `find { }` returns the first matching node; `findAll { }` returns every match. 2. `it` is the node under test, so `it.@variety` reads its attribute: `sprayDiary.block.find { it.@variety == 'Gala' }.@name`. 3. `**` — equivalently `depthFirst()` — searches at any depth, which is how you reach a node whose full ancestry you would rather not spell out: `**.find { it.@product == 'captan' }`. Because it is real Groovy, never assemble a path by pasting in a value someone else controls. Keep the path a literal and let the matcher do the comparing. ## Typed reads and the node model `XmlPath` gives typed accessors instead of casts, and two of them hand back REST Assured's own node objects rather than strings: | Call | Returns | |---|---| | `getString(path)`, `getInt(path)`, `getLong(path)` | one coerced value | | `getList(path)`, `getList(path, Class)` | a `List`, optionally element-typed | | `getMap(path)` | a `Map` | | `getNode(path)` | `Node` | | `getNodeChildren(path)` | `NodeChildren` | `io.restassured.path.xml.element.Node` declares `attributes()`, `children()`, `name()`, `value()` and `getAttribute(String)` — and `getAttribute` accepts either `"variety"` or `"@variety"`. `NodeChildren` adds `get(int)`, `size()`, `isEmpty()`, `list()`, `nodeIterable()` and `nodeIterator()`. Both extend `PathElement`, which contributes `get(name)`, `getPath(path)`, `getPath(path, Class)`, `getNode(name)` and `getNodes(name)`. That last group matters in practice: once you hold a `Node` you can keep pathing relative to it instead of re-walking from the root every time. It is also the difference between an assertion and a traversal — `body(path, matcher)` is the right tool when you know what you expect, while `getNode`/`getNodeChildren` are what you reach for when a test has to walk a variable number of blocks and decide something per node. ## Anchoring a path `XmlPath.setRootPath(String)` fixes a prefix so every later expression is relative to it — `new XmlPath(xml).setRootPath("sprayDiary.block[0]")`, then `getString("@name")`. The older `setRoot(String)` on the same class is `@Deprecated` and survives only for source compatibility, so write `setRootPath`. ## What actually goes wrong - Writing XPath out of habit — slashes, `//`, or `[@name='x']` predicates. None of that parses as GPath. - Dropping the `@` and reading an attribute as if it were a child element, which quietly matches nothing instead of failing loudly. - Asserting `text()` against a multi-node set and being surprised by a run-together string. - Expecting a missing attribute to blow up: `getString("block[3].@name")` on a two-block document returns `null`. - Assuming `getNode(...)` yields text. It yields a `Node`; call `value()`, `getAttribute(...)` or `getPath(...)` on it. - Reaching for the deprecated `setRoot(...)` because an old blog post used it. - Forgetting the quotes on an attribute whose name contains a hyphen, so Groovy reads `@applied` minus `on` instead of one attribute name. - Building the path string by concatenating a value from the test's own fixtures, which turns a data change into a compile-and-run of unintended Groovy.
- Why does asserting text() on sprayDiary.block.application.product give you one run-together string?Because the path matches a node set, and `text()` on a node set returns the concatenation of every match with no separator — `captansulphur` here. Assert on the element step itself with a collection matcher such as `hasItems(...)`, or index down to a single node first with `[0]`, when you want one value.
- What does XmlPath.getNodeChildren(path) give you that getList(path) does not?`getList` coerces each match to a value. `getNodeChildren` returns a `NodeChildren`, whose `list()`, `get(int)` and `nodeIterable()` hand you `Node` objects — so you keep `name()`, `value()`, `attributes()` and `getAttribute(...)` and can path further with `getPath(...)` relative to that node.
saying these in an interview costs you the question
- Thinks REST Assured XML paths are XPath expressions
- Uses slash syntax like /a/b/c instead of dotted steps
- Reads an attribute as a child element, without the @
- Expects a missing attribute to throw rather than return null
- Believes text() is required on every element read
- Names the deprecated setRoot instead of setRootPath