How do you assert on a JSON response body in REST Assured — including nested fields, arrays and "the whole shape" of the payload?
answer
- GPath not JSONPath — no $ prefix
- items.name maps over the list
- items.size(), items.find { it.id == 7 }
- rootPath / detachRootPath to cut repetition
- hasItems = subset, contains = exact
basics
~20 sUse then().body(path, matcher). The path is a GPath expression — user.address.city, items[0].id, items.size(), items.name — and the matcher is Hamcrest (equalTo, hasItems, containsInAnyOrder). Use rootPath to avoid repeating a prefix, and matchesJsonSchemaInClasspath for whole-payload shape.
solid answer
~40 sBody assertions are `then().body("<gpath>", <hamcrest matcher>)`. - **Nested fields**: `body("user.address.city", equalTo("Berlin"))` — dotted GPath, no `$.` prefix (it is GPath, not JSONPath). - **Arrays**: index with `body("items[0].id", equalTo(7))`, take the last with `items[-1]`, count with `body("items.size()", is(3))`, and collect a field across all elements with `body("items.name", hasItems("a", "b"))`. - **Filtering**: GPath is Groovy, so `body("items.find { it.id == 7 }.name", equalTo("a"))` and `findAll {}` work. - **Multiple assertions**: chain `body(...)` calls, or pass path/matcher pairs in one call. - **Repeated prefix**: `then().rootPath("data.user").body("name", equalTo("ann")).body("id", notNullValue())`. - **Whole shape**: `body(matchesJsonSchemaInClasspath("user-schema.json"))` from the `json-schema-validator` module, which catches added/removed/retyped fields that field-by-field assertions miss. Assert on meaning, not on every field; over-asserting makes the suite break on harmless changes.
code
java · 17 linesimport static io.restassured.RestAssured.given;
import static io.restassured.module.jsv.JsonSchemaValidator.matchesJsonSchemaInClasspath;
import static org.hamcrest.Matchers.*;
given()
.queryParam("active", true)
.when()
.get("/orders")
.then()
.log().ifValidationFails()
.statusCode(200)
.body("items.size()", is(3))
.body("items.name", hasItems("widget", "gadget"))
.body("items[0].price", equalTo(9.99f))
.body("items.find { it.id == 7 }.status", equalTo("SHIPPED"))
.body("customer.address.city", equalTo("Berlin"))
.body(matchesJsonSchemaInClasspath("schemas/orders.json"));go deeper
Know the body(path, matcher) form, dotted nesting, and simple array indexing; know that the matchers are Hamcrest.
Add the list-mapping behaviour of GPath, size()/find{}, rootPath, and the numeric type gotcha with equalTo.
Argue about what deserves an assertion: behaviour-carrying fields plus one schema check, with volatile values matched loosely so the suite does not rot.
Position schema assertions as the contract guard across services and decide where response shape is owned — schema files, generated clients, or consumer-driven contracts — rather than duplicated in test bodies.
## The core call Everything about JSON body validation in REST Assured funnels through one method: ```java then().body("<path expression>", <hamcrest matcher>) ``` Two independent pieces do the work: the **path** selects data out of the parsed body, and the **matcher** decides whether the selected data is acceptable. ## The path language is GPath, not JSONPath This trips up nearly everyone coming from JSONPath tooling. REST Assured parses the body with Groovy and evaluates **GPath** expressions against it. Practical consequences: - No `$` root prefix: write `user.name`, not `$.user.name`. - Array indexing uses `items[0]`, and negative indexes work: `items[-1]` is the last element. - A property access on a **list** maps over it: `items.name` yields the list of all names, not one name. So `body("items.name", hasItems("ann", "bob"))`. - Groovy methods are available: `items.size()`, `items.find { it.id == 7 }`, `items.findAll { it.active }.size()`, `items.collect { it.price }.sum()`. - Keys that are not valid Groovy identifiers (hyphens, dots inside the key) must be quoted: `body("'content-type'", ...)` or `body("data.'first-name'", ...)`. - If the top level of the response is an array, index it directly: `body("[0].id", equalTo(7))` and `body("size()", is(3))`. GPath evaluation requires the response to be parseable, which requires REST Assured to know the content type. If a server returns JSON with an unusual content type, either set `.accept(JSON)` or configure `RestAssured.defaultParser = Parser.JSON`, otherwise path assertions fail with a "cannot parse" style error rather than a value mismatch. ## The matcher side is Hamcrest Any Hamcrest matcher works, and composition is where the expressiveness comes from: - Scalars: `equalTo`, `is`, `notNullValue`, `containsString`, `startsWith`, `greaterThan`. - Collections: `hasSize`, `hasItems` (subset, order-independent), `contains` (exact list, in order), `containsInAnyOrder` (exact list, any order), `everyItem(...)`. - Composition: `both(...).and(...)`, `anyOf(...)`, `not(...)`. The distinction between `hasItems` and `contains`/`containsInAnyOrder` matters: `hasItems` passes if the extra elements exist, so it will not catch an unexpectedly longer list. Choose deliberately. Type-awareness is a common gotcha. JSON numbers are deserialized to `Integer`, `Float` or `BigDecimal` depending on value and configuration, so `body("price", equalTo(9.99))` can fail against a `Float` even though the printed values match. Safer options are `equalTo(9.99f)`, `is(closeTo(new BigDecimal("9.99"), new BigDecimal("0.001")))`, or configuring `JsonConfig.jsonConfig().numberReturnType(BIG_DECIMAL)`. ## Reducing repetition When many assertions share a prefix, `rootPath` (older name: `root`) removes the noise: ```java then() .rootPath("data.user") .body("name", equalTo("ann")) .body("address.city", equalTo("Berlin")) .detachRootPath("address"); ``` You can also pass alternating path/matcher pairs to a single `body(...)` call, and parameterize a root with `rootPath("items[%d]")` + `body("id", withArgs(0), equalTo(7))`. ## Asserting the whole shape Field-by-field assertions verify the fields you thought of. They will not notice that the API silently dropped a field, renamed one, or started returning a string where a number used to be — unless a test happened to assert on exactly that field. Two complements: - **JSON Schema validation**: add the `json-schema-validator` module and assert `body(matchesJsonSchemaInClasspath("user-schema.json"))`. The schema encodes required fields, types and formats once, and every response is checked against it. This is the cheapest guard against accidental contract drift. - **Deserialize and compare**: `response.as(User.class)` and then assert on the object, which fails loudly if a required field cannot be bound. Useful when you already own a client model. ## Choosing what to assert The most common practical mistake is over-assertion: pinning every field of every response, including server-generated ids and timestamps. Such a suite breaks on every harmless change and trains people to "fix" tests by pasting in the new value. A healthier pattern is: assert the status code, assert the small set of fields that carry the behaviour under test, use `notNullValue()`/matchers for volatile values like ids and timestamps, and let one schema assertion cover the overall shape. ## When the assertion fails The error message names the path, the matcher's description and the actual value — but not the full body. Adding `.log().ifValidationFails()` on the response side (or `.log().all()` while developing) prints the actual payload, which is usually the fastest way to see that the path was wrong rather than the value.
- An assertion body("price", equalTo(9.99)) fails even though the response clearly contains 9.99. Why?Hamcrest's `equalTo` compares with `equals()`, which is type-sensitive, and REST Assured deserializes JSON numbers to `Float` (or `Integer`/`BigDecimal` depending on the value and configuration). A `Float` 9.99f is not `equals` to a `Double` 9.99, so the comparison fails while the printed values look identical. Fix it by matching the actual type (`equalTo(9.99f)`), by using `closeTo` on a `BigDecimal`, or by setting `JsonConfig.jsonConfig().numberReturnType(BIG_DECIMAL)` so all numbers come back as one predictable type.
- Why add a JSON Schema assertion if you already assert on the individual fields you care about?Field assertions only cover fields somebody thought to assert; they are blind to a dropped, renamed or retyped field elsewhere in the payload. A schema assertion states the required fields and their types once and checks every response against it, catching accidental contract drift cheaply. It also documents the response shape in one reviewable file instead of scattering it across dozens of test methods.
saying these in an interview costs you the question
- Writing JSONPath syntax with a $ prefix and assuming it works — the path language is GPath.
- Expecting items.name to return a single value rather than the list of all names.
- Using hasItems to assert an exact list, then being surprised the test passes when extra elements appear.
- Asserting on every field including ids and timestamps, then updating expected values whenever the suite goes red.
- Assuming body() paths work regardless of content type — the response must be parseable as JSON, which depends on the content type or the configured default parser.