skip to content

In REST Assured, why does the JsonPath expression $.glossary.entry[0].term return null?

level: middleimportance: should knowfreq 48%

answer

  1. Groovy notation, not the other library
  2. no root token in the path
  3. closures, not bracket filters
  4. a missing key silently yields null
  5. brackets index, braces filter

basics

~20 s

REST Assured's JsonPath speaks Groovy GPath, not the Jayway JsonPath syntax other tools use. GPath has no dollar-sign root, so that fragment is read as an ordinary property name, finds nothing, and the chain collapses to null. Write glossary.entry[0].term instead.

solid answer

~50 s

REST Assured's `io.restassured.path.json.JsonPath` is a **Groovy GPath** evaluator, not an implementation of Jayway's JsonPath library, and the two syntaxes only look alike. GPath has no `$` root token: your path is concatenated onto an internal root variable, so `$.glossary...` compiles as a property lookup for a field literally named `$`. That field is absent, the lookup yields `null`, and each following dot is a property read on `null` — which the evaluator deliberately reports as `null` rather than as an error, so it reads like a data problem. Spell the path the plain dotted way instead: `glossary.entry[0].term`, `glossary.entry[-1].term` for the last element, `glossary.entry.size()`, and closures such as `glossary.entry.findAll { it.confidence < 0.5 }.term`. A Jayway bracket filter fails louder — `entry[?(@.confidence<0.5)]` is not valid Groovy, so it throws `IllegalArgumentException` with a message beginning *Invalid JSON expression*.

code

java · 11 lines
java
given()
    .baseUri("https://glossary.example.test")
.when()
    .get("/glossaries/ui-strings")
.then()
    .statusCode(200)
    .body("glossary.entry[0].term", equalTo("checkout"))
    .body("glossary.entry[-1].term", equalTo("shipping"))
    .body("glossary.entry.size()", is(3))
    .body("glossary.entry.findAll { it.confidence < 0.5 }.term", hasItem("cart"))
    .body("glossary.entry.collect { it.reviewCount }.sum()", greaterThan(5));

go deeper

for a junior

Be ready to name the notation: REST Assured paths are Groovy GPath. Recall the three basics — dotted navigation, [0] and [-1] indexing, and a findAll { } closure with the implicit it.

for a middle

Explain the mechanics: the path is concatenated onto a root variable and compiled as Groovy, which is why an unknown key yields null rather than an exception and why a bracket filter fails to compile.

for a senior

Show how you debug a silent null in CI: shorten the path segment by segment, print the parsed body once, and confirm the dialect before blaming the service. Say what you would put in a team cheat sheet.

for a principal

Own the consistency argument. Paths are untyped strings scattered across a suite; decide whether they live inline, in shared constants, or behind typed accessors, and what a reviewer is allowed to approve.

## The expression language is Groovy GPath `io.restassured.path.json.JsonPath` — the class behind `then().body(path, matcher)`, `extract().path(...)` and the standalone `JsonPath.from(json)` — evaluates **Groovy GPath**, the navigation notation Groovy uses for structured documents. It is not an implementation of the Jayway `JsonPath` library. The class names collide, the two notations agree for a trivial path like `glossary.key`, and they diverge the instant you index, filter or project. REST Assured's own documentation states the distinction explicitly, and guessing the wrong one is the most common cause of "my path returns null". The worked payload below is a translation glossary served from `GET /glossaries/ui-strings`: ```json { "glossary": { "key": "ui-strings", "sourceLocale": "en-GB", "entry": [ { "term": "checkout", "translation": "Kasse", "targetLocale": "de-DE", "confidence": 0.82, "reviewCount": 3 }, { "term": "cart", "translation": "Panier", "targetLocale": "fr-FR", "confidence": 0.41, "reviewCount": 0 }, { "term": "shipping", "translation": "Envio", "targetLocale": "es-ES", "confidence": 0.95, "reviewCount": 7 } ] } } ``` ## Why the dollar sign produces null instead of an error The evaluator does three things with the string you hand it: 1. It escapes the fragments that are not legal Groovy identifiers — a fragment containing a hyphen, one beginning with `@` and one beginning with a digit are wrapped in single quotes, and one containing `class` is rewritten as a `getAt(...)` call. 2. Unless the path already starts with a bracket index, it concatenates the path onto an internal root variable, producing `<root>.<your path>`. 3. It compiles that one-line script as Groovy and runs it with the parsed document bound to the root name. So `$.glossary.entry[0].term` becomes `<root>.$.glossary.entry[0].term`. `$` is a perfectly legal Groovy identifier, so this is a property read for a map key named `$` — which does not exist and returns `null`. Every dot after that is a property read on `null`, and the evaluator deliberately converts that particular `NullPointerException` into a `null` result so that a genuinely missing field does not explode an assertion. Your matcher then fails with "expected ... but was null", which looks like bad test data and is actually bad syntax. A Jayway *filter* is louder: `glossary.entry[?(@.confidence<0.5)]` is not valid Groovy at all, compilation fails, and REST Assured rewrites Groovy's `startup failed:` prefix into an `IllegalArgumentException` whose message begins **Invalid JSON expression:**. ## The GPath vocabulary you actually need - `glossary.sourceLocale` — dotted property navigation, read exactly as written. - `glossary.entry[0].term` — positional index into a list; `glossary.entry[-1].term` counts from the end. - `glossary.entry.term` — a property read on a **list spreads**, returning the list of all three terms. - `glossary.entry.size()` — ordinary Groovy methods are callable inside the path. - `glossary.entry.find { it.targetLocale == 'fr-FR' }` — the first match; `it` is the implicit closure parameter. - `glossary.entry.findAll { it.confidence < 0.5 }.term` — every match, then spread to a list of terms. - `glossary.entry.collect { it.reviewCount }.sum()` — map then reduce; `glossary.entry.reviewCount.sum()` is shorter. - Use single quotes for string literals inside the path, because the path itself is a double-quoted Java string. - There is no depth-first `**` shortcut on the JSON side — that escaper is installed only by the XML evaluator. | Intent on the glossary payload | GPath expression | Result | |---|---|---| | first term | `glossary.entry[0].term` | `"checkout"` | | last term | `glossary.entry[-1].term` | `"shipping"` | | all terms | `glossary.entry.term` | list of three strings | | how many entries | `glossary.entry.size()` | `3` | | low-confidence terms | `glossary.entry.findAll { it.confidence < 0.5 }.term` | `["cart"]` | | total reviews | `glossary.entry.collect { it.reviewCount }.sum()` | `10` | ## What 6.0.0 changed, and what it did not This is the part most write-ups get backwards. - The `json-path` module was **migrated fully to Java** in 6.0.0; its `src/main` no longer contains a `groovy` directory at all. - The evaluator stopped using a `GroovyShell`. It now compiles the path with `GroovyClassLoader.parseClass(...)` and runs it through `InvokerHelper.createScript(...)`, which fixed memory leaks seen in long-running processes that used `JsonPath` heavily. - `GroovyShell` now appears in exactly one main source file in the whole project, and it is on the XML side. - **The expression language did not change.** The path is still compiled and executed as Groovy, GPath closures still work, and interpolating untrusted input into a path is still code injection. - The `JsonPath` class Javadoc still says the implementation "uses a Groovy shell". That sentence is stale. So "6.0 removed Groovy from JSON path evaluation" is wrong twice: Groovy is still the language, and what was replaced was one particular host class, not the engine. ## Practical consequences Treat the path as code, because it is. Keep a small set of known-good idioms in the team's head rather than copying snippets from tutorials that were written against a different library, and when an expression returns `null`, first ask whether the syntax is even the right dialect before you go hunting for a data bug.

  • How would you tell a silently-wrong path apart from a genuinely absent field?
    Evaluate the path in pieces. Ask for the parent first — `glossary.entry` — and check it is a list of the expected size; then add one segment at a time. The segment that first turns the result into `null` is the broken one. A field that really is absent behaves the same way, so confirm against the raw body once before you change the path.
  • What happens when a JSON field name is not a legal Groovy identifier, such as source-locale?
    The evaluator escapes it for you before compiling: a fragment containing a hyphen is wrapped in single quotes, so `glossary.'source-locale'` is what actually runs. Fragments starting with a digit get the same quoting, and one containing `class` is rewritten as a `getAt(...)` call. You can also write the quotes yourself, which is clearer to a reviewer.

Two dialects that share a lot of words: you can order coffee in either, but ask for anything specific and one of them quietly hands you nothing instead of correcting you.

saying these in an interview costs you the question

  • Assuming REST Assured implements the Jayway JsonPath syntax
  • Starting a path with $ because every online snippet does
  • Writing bracket filters like [?(@.price<10)] instead of a closure
  • Reading a null result as missing data rather than a wrong dialect
  • Believing 6.0 removed Groovy from JSON path evaluation