JSONPath Expressions for Extraction

9 min read

The previous chapter closed with the assertion chain — body("name", equalTo("Alice")). That JsonPath in the first argument is a language, not just a string lookup. It can dive into nested objects, walk arrays by index, count, filter by predicate, and pull out single values you can keep as Java variables for later requests. This lesson is the JsonPath language in depth: the same engine the API Testing Masterclass lesson on JSONPath introduced, applied through Rest Assured's jsonPath() extractor and inside the body() matcher chain.

A reference response

Every example below works against this shape:

{
  "users": [
    { "id": 1, "name": "Alice", "role": "admin",  "address": { "city": "London" } },
    { "id": 2, "name": "Bob",   "role": "tester", "address": { "city": "Manchester" } }
  ],
  "total": 2,
  "page": 1
}

Two users, one admin and one tester, each with a nested address. Realistic enough to exercise every JsonPath feature.

Pulling out single values

Capture the response object and extract typed values with jsonPath():

import io.restassured.response.Response;
 
Response response = given().when().get("/users");
 
int total = response.jsonPath().getInt("total");                       // 2
String firstName = response.jsonPath().getString("users[0].name");     // "Alice"
String city = response.jsonPath().getString("users[0].address.city");  // "London"
int firstId = response.jsonPath().getInt("users[0].id");               // 1

The methods are typed (getInt, getString, getBoolean, getDouble, getLong) — Rest Assured handles the JSON-to-Java conversion. The expression is the same language whether you use it in body() or jsonPath().getXxx().

Lists and primitive arrays

getList() flattens an array of objects to a list of values when you ask for a field path:

List<String> names = response.jsonPath().getList("users.name");   // ["Alice", "Bob"]
List<Integer> ids  = response.jsonPath().getList("users.id");     // [1, 2]

users.name reads as: for every user in users, give me its name field. This implicit projection is where JsonPath earns its keep — it would take five lines of Java to do the same with Stream.map.

To get the raw array of user objects (each as a Map):

List<Map<String, Object>> users = response.jsonPath().getList("users");

Walking into nesting

Dotted paths walk objects; bracketed indices walk arrays. They compose freely:

.body("users[0].address.city", equalTo("London"))
.body("users[1].address.city", equalTo("Manchester"))
.body("page", equalTo(1))

Negative indices count from the end (users[-1] is the last user). And users.size() gives the array length without writing Java.

Groovy GPath — the killer feature

Rest Assured's JsonPath sits on top of Groovy's GPath, so collection operators like findAll, find, and collect work inside the expression. This is what turns "find the value at this path" into "find every record matching a predicate":

// Every admin (filter)
List<Map<String, Object>> admins = response.jsonPath()
    .getList("users.findAll { it.role == 'admin' }");
 
// The first match (single object)
Map<String, Object> alice = response.jsonPath()
    .getMap("users.find { it.name == 'Alice' }");
 
// Just the names of all admins (filter then project)
List<String> adminNames = response.jsonPath()
    .getList("users.findAll { it.role == 'admin' }.name");
 
// Count of admins
int adminCount = response.jsonPath()
    .getInt("users.findAll { it.role == 'admin' }.size()");

The it is the Groovy convention for "the current element." it.role == 'admin' is the predicate; everything after the closing brace continues the chain.

You only ever write Groovy inside these expressions — the surrounding code stays Java. Treat it like SQL embedded in a Java program: a small DSL for one specific job.

JsonPath in the body() chain

Every expression above also works in the assertion chain — Rest Assured runs them through the same engine:

.then()
    .statusCode(200)
    .body("users[0].name", equalTo("Alice"))
    .body("users.size()", equalTo(2))
    .body("users.findAll { it.role == 'admin' }.size()", equalTo(1))
    .body("users.name", hasItems("Alice", "Bob"))
    .body("users.address.city", hasItem("London"))
    .body("users.find { it.id == 2 }.name", equalTo("Bob"));

This is how you assert on collection invariants — exactly one admin, no user without an email, every order total > 0 — in one fluent chain.

Extract for chained requests

The most common reason to use extract() is to capture a value from one response and feed it into the next. The classic POST-then-GET pattern:

int userId = given()
    .contentType(ContentType.JSON)
    .body(newUser)
.when()
    .post("/users")
.then()
    .statusCode(201)
    .extract().jsonPath().getInt("id");
 
// Use the captured id in the next request
given()
    .pathParam("id", userId)
.when()
    .get("/users/{id}")
.then()
    .statusCode(200)
    .body("name", equalTo(newUser.getName()));

Two assertions, one captured value, no string manipulation. This pattern shows up in every CRUD test: create, capture the ID, read/update/delete by it.

The pattern: dots cross object boundaries, brackets cross array boundaries, and curly-brace blocks (Groovy GPath) filter or project across collections. That's the entire grammar.

Boolean expressions and existence checks

Groovy expressions can return booleans:

boolean hasAdmin = response.jsonPath()
    .getBoolean("users.any { it.role == 'admin' }");      // true if any user is admin
 
boolean allActive = response.jsonPath()
    .getBoolean("users.every { it.active == true }");     // true if every user is active

And to check whether a path is present at all:

.body("users[0].deletedAt", anyOf(nullValue(), is("")))   // tolerate null OR missing

A field that's absent from the JSON resolves to null in Rest Assured — there's no separate "missing" state.

When the expression starts to hurt

A JsonPath that takes thirty characters and a Groovy filter to read is usually a sign the test wants a typed model instead. extract().as(User.class) deserialises the response into a POJO you can assert on with normal getters — Chapter 5 covers this. Use raw JsonPath for one or two assertions; reach for POJOs when half your test is path strings.

⚠️ Common mistakes

  • Using single quotes inside the JsonPath where the API uses doubles. Inside Groovy expressions, it.role == 'admin' (single quotes) works because Groovy treats both quote styles as strings. But outside Groovy — e.g., for the path users[0]['name'] — Rest Assured uses dot notation; the bracket-quote form is a JSON Pointer thing, not GPath. Stick to users[0].name.
  • Calling .path() and .jsonPath().getXxx() interchangeably. .extract().path("users[0].name") returns Object — you must cast or trust the call site. .extract().jsonPath().getString("...") is typed. Prefer the typed form when the value goes into a Java variable.
  • Asserting on users[0] when the API doesn't guarantee order. users[0] happens to work in development when there's only ever one user, but breaks the moment the order changes. Use a Groovy filter (users.find { it.id == 42 }.name) when identity matters more than position.

🎯 Practice task

Drive every JsonPath shape against JSONPlaceholder. 30–40 minutes.

  1. GET /users. Use jsonPath().getList("name") to capture every user's name. Assert there are exactly 10 names and that "Leanne Graham" is in the list.
  2. GET /users. Inside .then(), assert body("[0].address.geo.lat", notNullValue()). Note how nested paths drill straight in.
  3. Filter with GPath. GET /users, then assert body("findAll { it.address.city.startsWith('S') }.size()", greaterThanOrEqualTo(1)). Read the Groovy until it makes sense — this is the most powerful matcher you'll write.
  4. Capture and reuse. GET /users/1. Extract the user's email to a Java string. POST that email back as part of a new comment body to /comments. Assert the response includes the email you sent.
  5. Get the admin's name. GET /users, find the user whose id == 5, and assert their name equals "Chelsey Dietrich" using a single GPath expression.
  6. Project a list. From /posts, extract every userId into a List<Integer>. Assert the list contains 100 elements and the unique values are 1 through 10.
  7. Boolean check. GET /users and assert that every user has a non-empty email using every { ... } inside a JsonPath.
  8. Stretch: force a path failure. Change one GPath to a wrong field name (users.zzname). Read the message — Rest Assured tells you the path that resolved to null. This is gold for debugging real responses.

Next lesson: Hamcrest matchers in depth — the equality, string, number, and collection matchers that turn vague assertions into precise specifications.

// tip to track lessons you complete and pick up where you left off across devices.