skip to content

In a Gatling simulation, how do you capture an order id from a JSON response body and send it on the next request, and what happens to both requests if the id is missing?

level: middleimportance: must knowfreq 74%

answer

  1. Capture on one request, reuse on the next
  2. jsonPath saveAs, then the placeholder
  3. saveAs only fires on a passing check
  4. Missing field produces two KO requests
  5. jsonPath is CoreDsl, status is HttpDsl

basics

~20 s

Attach a check to the first request: jsonPath("$.orderId").saveAs("orderId"), then read it back with the placeholder #{orderId} in the next request's URL or body. If the id is missing, the implicit exists() fails, nothing is saved, and both requests are KO.

solid answer

~40 s

On the request that returns the order, add `.check(jsonPath("$.orderId").saveAs("orderId"))`. `jsonPath` is a static on **`CoreDsl`**, not `HttpDsl`. That one line carries four steps: the check type, an implicit `find()` for the first occurrence, an implicit `exists()`, and the explicit save. The next request reads it back as `#{orderId}` — for example `.get("/orders/#{orderId}")`. `saveAs` is **only effective when the check passes**, so if the field is absent the implicit `exists()` fails, the first request is KO and nothing is written. The second request then cannot resolve `#{orderId}` either and fails in turn with `No attribute named 'orderId' is defined` — one root cause, two red rows in the report.

code

java · 28 lines
java
import io.gatling.javaapi.core.*;
import io.gatling.javaapi.http.*;
import static io.gatling.javaapi.core.CoreDsl.*;
import static io.gatling.javaapi.http.HttpDsl.*;

public class OrderCorrelationSimulation extends Simulation {

  HttpProtocolBuilder httpProtocol = http.baseUrl("https://shop.example.com");

  ChainBuilder checkout = exec(
      http("Place order")
        .post("/orders")
        .body(StringBody("{\"sku\":\"A1\",\"qty\":1}")).asJson()
        .check(status().is(201))
        .check(jsonPath("$.orderId")
                 .name("order id in create-order response")
                 .saveAs("orderId")))
    .exec(
      http("Fetch order")
        .get("/orders/#{orderId}")
        .check(status().is(200)));

  ScenarioBuilder scn = scenario("Checkout").exec(checkout);

  {
    setUp(scn.injectOpen(atOnceUsers(1))).protocols(httpProtocol);
  }
}

go deeper

for a junior

Be ready to write the capture and the reuse in one breath: a jsonPath check with saveAs, then the same key as a placeholder in the next request.

for a middle

Explain that saveAs fires only on a passing check, and that a missing field therefore produces two failed requests from one cause.

for a senior

Show how you would name the check and trim the actual value so that a per-user order id does not explode the report's error table.

for a principal

Own the team convention for which correlated values must be checked explicitly and which may ride the implicit exists().

Correlation — taking a value out of one response and putting it into the next request — is what the check pipeline exists for, and in Gatling it is one line on the first request plus one placeholder on the second. ## The capture ```java exec(http("Place order") .post("/orders") .body(StringBody("{\"sku\":\"A1\",\"qty\":1}")).asJson() .check(status().is(201)) .check(jsonPath("$.orderId").saveAs("orderId"))) .exec(http("Fetch order") .get("/orders/#{orderId}") .check(status().is(200))) ``` `jsonPath` is a static on **`CoreDsl`**; only `status` comes from `HttpDsl`. Both static imports have to be present, along with the two package wildcards that supply the types. ## What that one line actually declares Written out, `jsonPath("$.orderId").saveAs("orderId")` is four of the six check steps: 1. **check type** — `jsonPath("$.orderId")`; 2. **extraction** — implicit `find()`, identical to `find(0)`, so the **first** match wins if the path is not unique; 3. **validation** — implicit `exists()`; 4. **saving** — `saveAs("orderId")`, whose key must be a **static String**. That is why a capture is also an assertion in Gatling. You did not ask for the order id to be present; the DSL asked for you. ## When the field is missing The docs are explicit that saving *"is only effective when the check is successful: it could match the response and passed validation"*. Nothing partial happens: * the implicit `exists()` fails, so the check fails; * a failed check contributes no result, so **nothing is written to the Session**; * the request is reported **KO**, with a message beginning `jsonPath($.orderId).find.exists, found nothing`; * the next request's `#{orderId}` cannot resolve, and that request fails too with `No attribute named 'orderId' is defined`. One missing field therefore produces **two** failed requests with two different error messages, and only the first one names the real cause. Reading the earlier of the two rows is the whole diagnosis. ## Two behaviours that surprise people * **Other checks on the same response still run.** Gatling walks the full list of checks even after one fails, reports only the **first** failure, and still applies the `saveAs` of every check that passed. A request can be KO and have written Session attributes. * **Checks do not run in the order you wrote them.** For HTTP, Gatling sorts checks by scope before running them — URL, then status, then header, then body, then time — so a `status()` check always runs before a body check whatever the source order. That is why the status failure, not the JSON failure, is usually the one reported when a server returns a 500 with an error payload. ## Making the failure informative One optional step pays for itself here, and a second is routinely added by mistake: * `name("order id present in create-order response")` replaces the auto-generated `jsonPath($.orderId).find.exists` in the error message and in the report's error table. * `logActualValueInError(false)`, added in **Gatling 3.15**, is **inert on this check**. It suppresses the actual value only for validators that compare against an expected one (`is`, `isNull`, `in`, `notExists`, `lt`/`lte`/`gt`/`gte`), turning their per-user `found <actual>` into a flat `failed`. The implicit `exists()` has no actual value to log: it reports the constant `found nothing`, already one error row however many users hit it. Put the flag on a check that *compares* the id, such as `jsonPath("$.id").isEL("#{orderId}")` on the follow-up request. ## If the id is genuinely optional Do not reach for a `try`-style wrapper. Say it in the pipeline instead: `optional()` in place of the implicit `exists()` never fails, and when nothing matched the remaining steps — `saveAs` included — do not fire, so a value already stored under that key is neither replaced nor removed. `withDefault("unknown")` is the other option when a placeholder value is better than nothing. ## The Scala spelling ```scala exec(http("Place order").post("/orders") .check(status.is(201)) .check(jsonPath("$.orderId").saveAs("orderId"))) .exec(http("Fetch order").get("/orders/#{orderId}")) ``` Same chain. The omitted steps arrive through implicit conversions declared in `io.gatling.core.check.CheckSupport` and brought into scope by `import io.gatling.core.Predef._`, rather than through the Java API's class hierarchy — which is why a Scala check that omits a step will not even compile without that import. Note also that Scala writes `status` without parentheses where the Java family writes `status()`. In Kotlin, `is` and `in` are reserved words, so the validation step is spelled `shouldBe` and `within`. ## Two related failure modes worth rehearsing * **Several matches.** With no extraction step Gatling takes the **first** match. If the JSON body contains an array of orders and your path is not anchored, you will correlate the wrong one, silently and repeatably. Anchor the path, or be explicit with `find(n)` — remembering the rank is zero-based — or capture the lot with `findAll()`. * **A 304 response.** Gatling filters out every body-scope and chunk-scope check when the response is `304 Not Modified`, because there is no body to read. The request is still OK — a 304 satisfies the default status check — but the `jsonPath` check never ran, so nothing was saved. A correlated value that vanishes only on the second iteration of a loop is worth checking for this. ## Why the capture is an assertion by default It is tempting to read the implicit `exists()` as Gatling being opinionated. It is the opposite: it puts the failure on the request that actually broke the contract. Take it away — by making every capture `optional()` — and the run goes green at the point of the break and red somewhere downstream, or nowhere at all if a stale value from a previous iteration is still sitting under the key. The default is what keeps the report diagnostic, and it is the reason correlation in Gatling is one line rather than a capture plus a separate assertion.

  • If the JSON body contains three matches for the path, which one does `saveAs` store?
    The first. With no extraction step declared, Gatling inserts `find()`, which is identical to `find(0)`. To store all of them use `findAll()`, which saves a list, or target a specific rank with `find(n)` using a zero-based index.
  • The create-order request is KO, yet a later step still sees a value from that response. How?
    Gatling runs every check on the response even after one fails. It reports only the first failure, but each check that passed still applies its own `saveAs`. So a KO request can legitimately have written Session attributes from its other checks.
  • Can the `saveAs` key be built at runtime, for example per user?
    No. `saveAs(key)` takes a static String only, as does `name(...)`. If you need per-user separation, store a single value under a fixed key and let each virtual user's own Session keep them apart — Sessions are already per user.

saying these in an interview costs you the question

  • Expecting saveAs to write something when the check itself failed
  • Blaming the second request when the first request lost the value
  • Importing jsonPath from HttpDsl instead of CoreDsl
  • Assuming the first match is not chosen when several paths match