JSON Path Extraction and Validation

9 min read

Real responses aren't flat. They're nested objects with arrays of objects with more nested objects. Asserting on them — and pulling values out for use in the next request — is where Postman tests start to feel like real code. This lesson is about navigating JSON inside the Tests tab, validating shapes deeply, storing extracted values for chained requests, and running full-blown JSON Schema validation. The masterclass JSON Schema Validation lesson covered the why; this is the Postman-flavoured how.

The data extraction loop

Most body tests follow the same four-step rhythm: parse the JSON, navigate to the value, validate it, and (if you'll need it later) store it as a variable.

Parse, navigate, validate, store. The same shape covers everything from a one-line id extraction to a 30-line schema check.

Parse and navigate

Start every body test by parsing once and navigating with regular JavaScript:

const response = pm.response.json();
const userId = response.data.user.id;
const firstOrderTotal = response.data.user.orders[0].total;
const orderItemCount = response.data.user.orders[0].items.length;

JSON nesting maps directly to JS property access:

  • Object key → .key (or ["key"] if the key has special characters).
  • Array index → [i] (zero-indexed).
  • Walk through dots and brackets exactly as the JSON nests.

If a path might be missing — common for optional fields — guard with optional chaining (?.) or a quick null check:

const orderTotal = response?.data?.user?.orders?.[0]?.total ?? 0;

The ?. returns undefined instead of throwing if any step in the chain is missing. The ?? 0 provides a default. Without those, a missing field somewhere mid-path would crash the entire test script with a TypeError.

Storing extracted values

The point of extraction often isn't just to assert on the value — it's to use it in a later request. Postman makes this trivial with the variable scopes you learned in Chapter 2:

const response = pm.response.json();
 
pm.environment.set("authToken", response.token);
pm.environment.set("userId", response.user.id);
pm.collectionVariables.set("lastOrderId", response.orders[0].id);

The next request can now reference {{authToken}}, {{userId}}, and {{lastOrderId}} anywhere — URL, headers, body, params. We'll wire this into a full chain in Chapter 4 Lesson 1.

The choice of scope mirrors the variable rules:

  • Environment — scope-tied (a token tied to staging).
  • Collection — scope-free (an id you keep using regardless of environment).
  • Globals — almost never. Globals leak everywhere.

Validating nested structures

A full structural assertion validates types, required keys, and value constraints in one block:

pm.test("User has the correct structure", () => {
    const user = pm.response.json().data;
 
    pm.expect(user).to.have.all.keys("id", "name", "email", "role", "createdAt");
 
    pm.expect(user.id).to.be.a("number");
    pm.expect(user.name).to.be.a("string");
    pm.expect(user.email).to.match(/.+@.+\..+/);
    pm.expect(user.role).to.be.oneOf(["admin", "user", "viewer"]);
    pm.expect(user.createdAt).to.match(/^\d{4}-\d{2}-\d{2}T/); // ISO date prefix
});

That's a "shape lock" — when the backend introduces a new field, to.have.all.keys fails and the test points you at the change. Some teams prefer to.include.all.keys(...) instead, which allows extra fields but still requires the listed ones. Pick whichever matches your culture: strict locks catch silent additions; loose locks reduce churn.

Validating arrays of objects

The pattern: assert the array has the expected shape, then loop and assert each element:

pm.test("Products list is valid", () => {
    const products = pm.response.json().products;
 
    pm.expect(products).to.be.an("array").with.lengthOf.above(0);
 
    products.forEach((product, index) => {
        pm.expect(product, `product at index ${index}`).to.have.property("id");
        pm.expect(product.price).to.be.a("number").and.to.be.above(0);
        pm.expect(product.name).to.be.a("string").and.to.have.lengthOf.above(0);
        pm.expect(product.inStock).to.be.a("boolean");
    });
});

Two things worth noticing:

  • The second arg to pm.expect(value, "message") is a label — when an assertion fails, the error includes the label. Without it, "expected null to be a number" doesn't tell you which product was broken.
  • Looping with forEach means a single broken product fails the whole test. That's usually what you want — you'd rather know "products[7] is missing a price" than have all 50 tests pass except one.

JSON Schema validation in Postman

For complex responses, hand-rolling assertions gets repetitive. JSON Schema is a standardised vocabulary for describing JSON shapes — type, required fields, formats, ranges, the lot. Postman ships the tv4 validator out of the box.

const schema = {
    type: "object",
    required: ["id", "name", "email"],
    properties: {
        id: { type: "number" },
        name: { type: "string", minLength: 1 },
        email: { type: "string", format: "email" },
        role: { type: "string", enum: ["admin", "user", "viewer"] },
        createdAt: { type: "string", format: "date-time" }
    },
    additionalProperties: false
};
 
pm.test("Response matches user schema", () => {
    const valid = tv4.validate(pm.response.json(), schema);
    pm.expect(valid, JSON.stringify(tv4.error)).to.be.true;
});

Two practical notes:

  • additionalProperties: false means exactly these fields. Drop it if extras are allowed.
  • The second argument to pm.expect (tv4.error) is what gets shown when the schema fails — without it, you'd just see "expected false to be true" and no idea what mismatched. Always pass the error in.

When does schema validation pay for itself?

  • The response has more than ~5 fields.
  • The same shape is returned by multiple endpoints (define the schema once, reuse it).
  • The team owns an OpenAPI spec — the schemas come for free from the spec's components.schemas.

For tiny responses, hand-written pm.expect is faster to read. Both approaches coexist happily in the same collection.

Reusing schemas across requests

If you're using the same schema in multiple requests, store it as a collection variable (yes, you can store objects — Postman serialises them):

// Pre-request script (or once in a setup test)
pm.collectionVariables.set("userSchema", JSON.stringify({
    type: "object",
    required: ["id", "name", "email"],
    properties: { /* ... */ }
}));
// Tests tab anywhere it's needed
const schema = JSON.parse(pm.collectionVariables.get("userSchema"));
pm.test("Matches user schema", () => {
    pm.expect(tv4.validate(pm.response.json(), schema)).to.be.true;
});

Schemas tend to grow; keeping the canonical version in one place is worth the indirection.

A full extract-and-validate example

Putting it all together — a test on POST Create User that validates the response, locks the shape, and stores the new id for chaining:

const response = pm.response.json();
 
pm.test("Status is 201 Created", () => {
    pm.response.to.have.status(201);
});
 
pm.test("Response has the user shape", () => {
    pm.expect(response).to.have.all.keys("id", "name", "email", "createdAt");
    pm.expect(response.id).to.be.a("number");
    pm.expect(response.email).to.match(/.+@.+/);
});
 
pm.test("Created user matches what we sent", () => {
    const sent = JSON.parse(pm.request.body.raw);
    pm.expect(response.name).to.equal(sent.name);
    pm.expect(response.email).to.equal(sent.email);
});
 
// Store for the next request in the chain.
pm.collectionVariables.set("createdUserId", response.id);
console.log("Stored createdUserId =", response.id);

The console.log shows up in the Postman Console (Cmd/Ctrl+Alt+C) — leave one in any extraction script while you're learning so you can verify the value was captured.

⚠️ Common mistakes

  • No null-guarding nested paths. body.data.user.id crashes the whole script if body.data is null. Use body?.data?.user?.id or guard with an if.
  • Forgetting to JSON.parse the request body. pm.request.body.raw is a string. Compare it to the response with JSON.parse(pm.request.body.raw), not against the string directly.
  • Schema with no error message. A passing schema test is silent; a failing one shows "expected false to be true" without tv4.error as the second arg. Always pass tv4.error (or an equivalent) so failures are debuggable.

🎯 Practice task

Extract, validate, and store. 30 minutes.

  1. Open GET User by ID (/users/1). In the Tests tab, parse the body and write three assertions: id is a number, email matches /@/, address.city is a non-empty string.
  2. Add a "shape lock" test using pm.expect(body).to.have.all.keys(...) listing every field the JSONPlaceholder /users/1 response returns. Send. Adjust the list until the test passes. Now your test will fail loudly if the API ever changes the shape.
  3. Open POST Create Post. After the existing assertions, add: pm.collectionVariables.set("createdPostId", pm.response.json().id); Send. Open the eye-icon variable view — confirm createdPostId has a value.
  4. Now open GET User by ID again. Change the URL to {{baseUrl}}/posts/{{createdPostId}}. Send. The variable from step 3 should resolve and you should get the post you "created". (JSONPlaceholder is faking it; the principle is what matters.)
  5. Schema validation. Open GET User by ID. Paste this schema-validation block:
    const schema = {
        type: "object",
        required: ["id", "name", "email", "username"],
        properties: {
            id: { type: "number" },
            name: { type: "string", minLength: 1 },
            email: { type: "string" },
            username: { type: "string" }
        }
    };
    pm.test("Matches user schema", () => {
        pm.expect(tv4.validate(pm.response.json(), schema), JSON.stringify(tv4.error)).to.be.true;
    });
    Send. Test should pass. Now break the schema (e.g. set id: { type: "string" }) and re-send — the failure message should show the exact field that mismatched.
  6. Stretch: open GET All Users and write a forEach loop that schema-validates every user in the array against the same schema. (Hint: loop, validate, fail the test if any single one is invalid.)

You can now navigate, validate, and shuttle nested JSON around inside Postman. The next lesson covers the other JavaScript hook — pre-request scripts — for setting up dynamic data before the request goes out.

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