skip to content

In REST Assured, how do you put a runtime value into a JsonPath GPath expression safely?

level: seniorimportance: should knowfreq 34%

answer

  1. the path compiles as a script
  2. bind, do not concatenate
  3. an apostrophe breaks it first
  4. param returns a new instance
  5. no such hook on the then side

basics

~20 s

A JSON GPath expression is compiled and run as Groovy, so concatenating a runtime value into it is code injection. Bind the value safely with JsonPath.param(name, value) instead. The expression then references it by name inside the closure.

solid answer

~50 s

Never build the path by string concatenation. REST Assured compiles the whole expression as a Groovy script and runs it against the parsed document, so a value spliced into the text is executable code, not data — and even a benign apostrophe in a glossary term terminates the Groovy string literal and turns the path into a compile error. The supported mechanism is `JsonPath.param(name, value)`, which puts the value into the script's binding; the expression then refers to it as a bare identifier, for example `.param("locale", locale).getList("glossary.entry.findAll { it.targetLocale == locale }.term")`. Two things to remember: `param` returns a **new** `JsonPath`, so you must chain or reassign, and referencing an identifier you never bound throws `IllegalArgumentException` telling you to define it with `JsonPath.param(...)`. There is no `param` hook on the `then()` side, so hold a `JsonPath` when a path must carry a runtime value.

code

java · 9 lines
java
String requestedLocale = System.getenv("TARGET_LOCALE");

JsonPath glossary = get("/glossaries/ui-strings").jsonPath();

List<String> terms = glossary
    .param("targetLocale", requestedLocale)
    .getList("glossary.entry.findAll { it.targetLocale == targetLocale }.term");

assertThat(terms, hasItem("cart"));

go deeper

for a junior

Recall the rule and the API name: never concatenate a value into a path, use param(name, value) and reference the name inside the closure. Know that the path is executed, not just parsed.

for a middle

Explain why binding works: the value goes into the script's Groovy binding, so it is data the compiled script reads rather than text the compiler sees. Mention that param returns a new instance.

for a senior

Demonstrate the production stance: make concatenation a review-blocking pattern, keep paths as constants, and be able to name the exact exception that a dropped param call produces in CI.

for a principal

Own the framing across the estate. Argue that binding versus escaping is the same decision your teams already make for queries and shells, and decide where such rules are enforced by tooling rather than by review.

## The path is code, not a query string REST Assured's JSON path expressions are Groovy. The evaluator concatenates your path onto an internal root variable, compiles the result as a one-line Groovy script, and runs it with the parsed document bound in. That is what makes closures such as `findAll { it.confidence < 0.5 }` work at all — they are real Groovy closures, compiled and executed at assertion time. The consequence follows immediately: anything you splice into the path text becomes part of the program. Consider a helper on a translation-glossary suite that takes a locale from a fixture file, an environment variable or a CSV row: ```java // Do not do this List<String> terms = jsonPath.getList( "glossary.entry.findAll { it.targetLocale == '" + locale + "' }.term"); ``` If `locale` ever carries a quote, a brace or a method call, the compiled script is no longer the one you wrote. This is the ordinary injection shape — untrusted data crossing into an interpreter — and the library's own Javadoc calls it out. ## The failure modes, in the order you will meet them 1. **A quote in the data.** A French glossary term such as `l'article` closes the Groovy string literal early. Compilation fails and REST Assured reports `IllegalArgumentException` beginning *Invalid JSON expression:*. Harmless, noisy, and the first sign that the helper is built wrong. 2. **A brace or operator in the data.** The path still compiles, but it no longer means what you wrote. The assertion passes or fails for reasons unrelated to the response. 3. **Executable content in the data.** Because the fragment is compiled as Groovy, a value can invoke methods. Whether that is exploitable depends on where the value came from, but the mechanism is real and it is not defended by escaping quotes. ## The supported mechanism: JsonPath.param `JsonPath.param(String key, Object value)` adds the value to the map that becomes the script's Groovy binding. Inside the expression the value is then a plain identifier — no quoting, no escaping, no concatenation: ```java List<String> terms = jsonPath .param("targetLocale", requestedLocale) .getList("glossary.entry.findAll { it.targetLocale == targetLocale }.term"); ``` Points that catch people out: - `param` returns a **new `JsonPath` instance** rather than mutating the receiver. Calling it on its own line and then querying the original silently loses the binding, and you get the "not defined" error below. - Referencing an identifier you never bound throws `IllegalArgumentException` with the message *The parameter "x" was used but not defined. Define parameters using the JsonPath.param(...) function*. That message is the single clearest signal that a `param` call was dropped or not chained. - Bind as many parameters as you need by chaining `param` calls; each returns a further instance. - The value keeps its Java type in the binding, so a number compares as a number and a string as a string — which is also why you no longer have to think about quoting. - There is **no `param` hook on the validation side**. `then().body(path, matcher)` accepts a path and a matcher, nothing else, so a path that must carry a runtime value belongs on a `JsonPath` you hold — either standalone via `JsonPath.from(body)` or from the response object. | Approach | What the evaluator compiles | Verdict | |---|---|---| | `"... == '" + locale + "' }"` | your data, as Groovy source | injection; also breaks on an apostrophe | | `String.format` into the path | same as above, with nicer syntax | no safer; formatting is not escaping | | `.param("locale", locale)` | a fixed script plus a bound variable | correct | ## What 6.0.0 changed here — and what it did not The `json-path` module was migrated fully to Java in 6.0.0, and the evaluator stopped using a `GroovyShell`: it now compiles with `GroovyClassLoader.parseClass(...)` and runs the result through `InvokerHelper.createScript(...)`, which fixed memory leaks in long-running processes that leaned on `JsonPath`. It is tempting to read that as "Groovy is gone from JSON paths". It is not. The expression is still Groovy, still compiled, still executed — only the host class changed. The injection hazard is exactly as it was, and the `JsonPath` class Javadoc still describes a Groovy shell, which is now stale text rather than a description of the code. ## How to hold the line in a suite - Make concatenation into a path a review-blocking pattern, the same way string-built SQL is. - Keep paths as constants and vary only the bound parameters, so a grep for `" + ` inside a path string finds every offender. - Where the value is a fixed set — a locale from a known list, a status from an enum — validating it against that list is a good extra layer, but it is a second line of defence, not the fix. - Remember the general principle is not REST Assured's: data crossing into an interpreter must be bound, not escaped. `param` is simply this library's binding API.

  • What error tells you a param call was written but not chained?
    An `IllegalArgumentException` reading *The parameter "x" was used but not defined. Define parameters using the JsonPath.param(...) function*. Because `param` returns a new `JsonPath` rather than mutating the receiver, calling it on its own line and then querying the original produces exactly that message.
  • Is validating the value against an allowlist enough on its own?
    It reduces exposure but does not remove the defect. The call site still compiles data as source, so a later change to the allowlist, or a value that is legal but syntactically awkward, reopens it. Bind with `param` and treat the allowlist as defence in depth.
  • How do you parameterise a path used in a then().body(...) assertion?
    You do not — the validation side takes a path and a matcher and offers no binding hook. Pull the value out through a `JsonPath` you hold, bind the parameter there, and assert on the extracted result with an ordinary matcher.

saying these in an interview costs you the question

  • Escaping quotes in the value instead of binding it
  • Assuming a path is a query string, not compiled code
  • Calling param on its own line and querying the original instance
  • Believing 6.0 removed Groovy so injection is no longer possible
  • Assuming string formatting into a path is safer than concatenation
  • Expecting a param hook on the then() validation side