skip to content

In REST Assured, what do rootPath, appendRootPath, detachRootPath and noRootPath do to body expectations?

level: juniorimportance: should knowfreq 47%

answer

  1. a prefix for body paths only
  2. rootPath replaces, appendRootPath extends
  3. detach pops a suffix, not any substring
  4. noRootPath is rootPath with empty string
  5. root/noRoot short forms are deprecated

basics

~20 s

REST Assured's rootPath sets a prefix that every later body(path, matcher) call in the same then() chain is joined onto. appendRootPath extends that prefix, detachRootPath strips a suffix off it, and noRootPath clears it. They affect body expectations only.

solid answer

~50 s

They are the root-path family on `io.restassured.response.ValidatableResponse`, and they exist so you stop retyping a shared prefix. `rootPath("currentBid")` makes every following `body("amount", ...)` assert on `currentBid.amount`; calling `rootPath(...)` again **replaces** the prefix rather than extending it. `appendRootPath("bidder")` extends it to `currentBid.bidder`, `detachRootPath("bidder")` takes that suffix back off, and `noRootPath()` resets it to the empty string. Position in the chain matters: the prefix applies only to `body(...)` calls written after it. Merging inserts a dot unless the key already starts with `[`, so `rootPath("lots")` plus `body("[0].breed", ...)` reads `lots[0].breed`. Two failure modes are worth knowing — `detachRootPath` throws `IllegalStateException` when the current root does not end with the string you gave it, and again when there is no root at all. The prefix touches `body(...)` alone; `header(...)`, `cookie(...)` and `statusCode(...)` never see it. The short spellings `root()`, `appendRoot()`, `detachRoot()` and `noRoot()` are `@Deprecated`.

code

java · 15 lines
java
import static io.restassured.RestAssured.when;
import static org.hamcrest.Matchers.*;

when().get("/lots/LOT-4417")
.then()
    .rootPath("currentBid")
    .body("amount", equalTo(1875))
    .body("currency", equalTo("AUD"))
    .appendRootPath("bidder")
    .body("bidderId", equalTo("BID-88"))
    .body("registeredRing", equalTo(3))
    .detachRootPath("bidder")
    .body("placedAt", notNullValue())
    .noRootPath()
    .body("lotId", equalTo("LOT-4417"));

go deeper

for a junior

Know that rootPath sets a shared prefix for the body paths that follow, and be able to read a chain and say which full path a given body(...) line is asserting.

for a middle

Explain the merge rule, that rootPath replaces rather than appends, and that detachRootPath is a suffix pop that throws IllegalStateException when the prefix does not end with what you passed.

for a senior

Have a view on how deep to nest before readability suffers, and be able to spot the review smell of a chain whose prefix changes so often that no line can be read in isolation.

for a principal

Decide the house style: whether shared prefixes are set inline or hoisted, and how the team keeps deprecated spellings such as root() out of a growing suite.

A `then()` chain over a nested payload repeats itself fast. Against a cattle-auction bidding API, `GET /lots/LOT-4417` returns a lot whose interesting fields all live under one object: ```json { "lotId": "LOT-4417", "currentBid": { "amount": 1875, "currency": "AUD", "placedAt": "2026-03-11T09:14:02Z", "bidder": { "bidderId": "BID-88", "registeredRing": 3 } } } ``` Without help you would write `body("currentBid.amount", ...)`, `body("currentBid.currency", ...)` and so on. The root-path family on `io.restassured.response.ValidatableResponse` removes that repetition. ## The four calls - **`rootPath(String)`** sets the prefix. It **overwrites** whatever prefix was in force — it is not cumulative. - **`appendRootPath(String)`** merges the new fragment onto the current prefix and makes the result the new prefix. - **`detachRootPath(String)`** removes that fragment from the **end** of the current prefix, then trims a trailing dot. - **`noRootPath()`** is exactly `rootPath("")` — it clears the prefix. Each also has an overload taking a `List<Argument>`, which is how `withArgs(...)` fills `%s` and `%d` placeholders inside the prefix. ## How the prefix and the key are joined REST Assured does not blindly concatenate. Merging a key onto a non-empty prefix follows a small rule set: 1. If the prefix ends with a dot and the key begins with one, the duplicate dot is dropped. 2. If neither side carries a dot, a dot is inserted between them — `currentBid` plus `amount` becomes `currentBid.amount`. 3. If the key starts with `[`, `?[`, `?.` or `*.`, the two are concatenated with **no** dot, so `rootPath("lots")` plus `body("[0].breed", ...)` gives `lots[0].breed`. That third rule is what makes index and safe-navigation keys work under a prefix, and it is the one people forget when a merged path comes out malformed. ## Order in the chain is the whole contract The prefix is applied at the moment each `body(...)` call is registered, so it affects only the expectations written after it. Move a `rootPath(...)` line and you silently change which nodes are asserted. A chain therefore reads top to bottom as a small cursor walking the document, and `noRootPath()` is how you walk back to the top. The scope is narrow in two useful ways: - It applies to `body(...)` expectations only. `header(...)`, `cookie(...)`, `statusCode(...)` and `time(...)` are untouched, because the merge happens on the body-matcher key alone. - It lives on the response specification for this chain, so it does not leak into the next test in the class. ## The two ways it throws `detachRootPath` is the strict one, and both of its failures are an `IllegalStateException`: - Calling it while no prefix is set fails with a message about detaching a path when the root path is empty. - Calling it with a fragment the current prefix does not **end with** fails with a message naming both strings — asking to detach `bidder` from `currentBid.bidder.bidderId` will not work, because `bidder` is not the suffix. So `detachRootPath` is a pop operation, not a search-and-remove. If you want an arbitrary prefix, set it outright with `rootPath(...)`. ## Name the current spellings The library also carries `root()`, `appendRoot()`, `detachRoot()` and `noRoot()`. They still work and they are all marked `@Deprecated`; the `*Path` forms are the current names and are what you should write and expect in review. The same deprecation applies on the path classes, where `setRoot(String)` has been superseded by `setRootPath(String)`. ## What the family does not do It helps to be precise about the limits, because each of these is a plausible-sounding assumption that is simply false: - It does not shorten the failure message. A mismatch still prints the fully merged path, so a CI log shows `JSON path currentBid.amount doesn't match.` rather than the bare key you typed. - It does not validate the prefix. Setting a root that matches nothing is silent; you only find out when the first `body(...)` under it reports a null actual value. - It does not survive the chain. Each `then()` starts from the response specification's own prefix, so nothing you set in one test reaches the next. - It does not stack across calls. `rootPath("a")` followed by `rootPath("b")` leaves `b`, not `a.b`; only `appendRootPath` combines. ## Worked shape A chain that walks down and back up reads like this: - `rootPath("currentBid")` then `body("amount", equalTo(1875))` asserts `currentBid.amount`. - `appendRootPath("bidder")` then `body("bidderId", equalTo("BID-88"))` asserts `currentBid.bidder.bidderId`. - `detachRootPath("bidder")` then `body("placedAt", notNullValue())` asserts `currentBid.placedAt` again. - `noRootPath()` then `body("lotId", equalTo("LOT-4417"))` asserts the top-level field. Used this way the family buys real readability. Overused — a chain that changes prefix five times — it buys confusion instead, because a reader now has to run the cursor in their head to know what any one line asserts. Two or three levels is where it stays useful.

  • What happens if you call rootPath twice in the same then() chain?
    The second call replaces the first outright; the prefixes are not concatenated. Expectations written between the two calls keep the first prefix, and everything after the second call uses the new one. If you wanted them combined, `appendRootPath(...)` is the call that merges rather than replaces.
  • Does a root path affect a header or status code assertion in the same chain?
    No. The prefix is merged onto the key of a body expectation only, so `header(...)`, `cookie(...)`, `statusCode(...)` and `time(...)` behave exactly as they would with no prefix set. That is why mixing them freely into a rooted chain is safe.

Think of it as cd inside the response document: rootPath is an absolute cd, appendRootPath a step down, detachRootPath a step back up, and noRootPath a return to the top.

saying these in an interview costs you the question

  • Thinks a second rootPath call appends to the first
  • Expects detachRootPath to remove a fragment from anywhere in the prefix
  • Assumes the root path also prefixes header or cookie assertions
  • Writes root(), noRoot() or appendRoot() as if they were current API
  • Believes the prefix applies to body calls written before it in the chain