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?
answer
- Capture on one request, reuse on the next
- jsonPath saveAs, then the placeholder
- saveAs only fires on a passing check
- Missing field produces two KO requests
- jsonPath is CoreDsl, status is HttpDsl
basics
~20 sAttach 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 sOn 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 linesimport 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
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.
Explain that saveAs fires only on a passing check, and that a missing field therefore produces two failed requests from one cause.
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.
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