Lesson 3 of 9 · API Testing
JSON for Testers
JSON syntax and data types, the mistakes that break payloads, querying with JSONPath, validating with JSON Schema and asserting on JSON in API tests.
beginner 8 min read Updated
You will learn to
- Read and write valid JSON and spot common syntax errors
- Distinguish null, missing, empty and zero values and test each deliberately
- Extract values from nested responses with JSONPath and JavaScript
- Validate response structure with JSON Schema
Prerequisites
- HTTP Fundamentals
JSON is the default data format of web APIs. Request bodies, response bodies, configuration files, test data fixtures and test reports from tools like Playwright’s JSON reporter are all JSON or close relatives of it. As an API tester you will read hundreds of JSON documents a day, build payloads to send, and write assertions against nested structures. Getting fluent with JSON (its exact rules, its edge cases, and the tools for querying and validating it) pays off on every test you write.
What JSON is
JSON (JavaScript Object Notation) is a text format for structured data. It is defined by RFC 8259 and ECMA-404. Despite the name it is language-independent: Java, Python, C#, Go and every other mainstream language can read and write it.
A JSON document is a single value. The value is usually an object or an array, but "hello", 42 and true are also valid JSON documents on their own.
The six data types
| Type | Example | Notes |
|---|---|---|
| String | "Ana Silva" |
Double quotes only. Escapes like \", \\, \n, é |
| Number | 42, -3.5, 1.2e3 |
No leading zeros (007 is invalid), no NaN or Infinity |
| Boolean | true, false |
Lowercase only |
| Null | null |
Lowercase only |
| Object | {"id": 1, "active": true} |
Unordered set of key/value pairs; keys are strings |
| Array | [1, "two", null] |
Ordered list; items can be any type |
There is no date type. Dates are sent as strings, almost always in ISO 8601 / RFC 3339 format such as "2026-11-09T10:15:00Z", or occasionally as numeric Unix timestamps. Always check which format an API uses and that it is consistent across endpoints.
A realistic response combining all types:
{
"id": "ord_1001",
"status": "paid",
"total": 59.98,
"currency": "USD",
"isGift": false,
"couponCode": null,
"createdAt": "2026-11-09T10:15:00Z",
"customer": {
"id": "u_42",
"email": "ana@example.com"
},
"items": [
{ "sku": "BOOK-1", "qty": 2, "price": 19.99 },
{ "sku": "PEN-7", "qty": 4, "price": 5.0 }
]
}
Syntax mistakes that break payloads
JSON is stricter than JavaScript object syntax. These are the errors you will see most often, both in payloads you write and in bugs you find:
| Invalid | Why | Valid |
|---|---|---|
{'name': 'Ana'} |
Single quotes | {"name": "Ana"} |
{name: "Ana"} |
Unquoted key | {"name": "Ana"} |
{"a": 1,} |
Trailing comma | {"a": 1} |
{"a": 1 "b": 2} |
Missing comma | {"a": 1, "b": 2} |
{"a": undefined} |
undefined is not JSON |
{"a": null} or omit the key |
{"n": 007} |
Leading zeros | {"n": 7} |
// comment |
No comments in JSON | Remove it |
Paste any doubtful payload into the JSON Formatter or JSON Validator to get the exact error position.
Null, missing, empty and zero
These four look similar and mean different things. APIs frequently treat them inconsistently, which makes them productive test inputs:
| Value | JSON | Typical meaning |
|---|---|---|
| Missing | key not present | “Not provided” or “use the default” |
| Null | "phone": null |
“Explicitly no value” or “clear this field” |
| Empty | "phone": "" or "tags": [] |
“Present but empty” |
| Zero / false | "qty": 0, "isGift": false |
A real value that happens to be falsy |
For a PATCH endpoint in particular, missing usually means “leave unchanged” while null means “clear it”. Test both and check the stored result with a follow-up GET.
On the response side, check the API is consistent: does an absent phone appear as null, as "", or not at all? Whatever the choice, it should match the documentation and be the same everywhere.
Numbers: precision and type
- JSON numbers have no separate integer and float types, and the spec leaves precision to implementations. JavaScript parses every number as a 64-bit float, so integers above
9007199254740991(Number.MAX_SAFE_INTEGER) lose precision. APIs that use large numeric ids often send them as strings for this reason. - Floating-point arithmetic is inexact: in JavaScript,
0.1 + 0.2is0.30000000000000004. When asserting on money calculated by the client, compare with a tolerance or, better, test APIs that represent money as integer minor units (cents) or decimal strings. - Check type as well as value.
"total": "59.98"(string) and"total": 59.98(number) are different contracts.
Reading values from JSON
In JavaScript
const order = await res.json();
order.customer.email; // 'ana@example.com'
order.items[0].sku; // 'BOOK-1'
order.items.length; // 2
order.items.map((i) => i.sku); // ['BOOK-1', 'PEN-7']
order.items.reduce((sum, i) => sum + i.qty * i.price, 0); // 59.98 (floating point)
order.discount?.code; // undefined, no error
With JSONPath
JSONPath is a query language for JSON, standardised in RFC 9535 (2024). Tools such as Postman, REST Assured (which uses a related GPath syntax), JMeter and many assertion libraries support it.
| Expression | Result on the order above |
|---|---|
$.status |
"paid" |
$.customer.email |
"ana@example.com" |
$.items[0].sku |
"BOOK-1" |
$.items[*].sku |
["BOOK-1", "PEN-7"] |
$.items[?(@.qty > 2)].sku |
["PEN-7"] |
$..price |
[19.99, 5] (recursive descent: every price at any depth) |
$ is the root, . selects a child, [*] selects all items, .. searches recursively, and [?...] filters. The parentheses around the filter expression are optional in RFC 9535 ([?@.qty > 2]) but required by many older tools, so the parenthesised form is the most portable. Experiment in the JSONPath Tester.
Validating structure with JSON Schema
Checking a few values is not enough. You also want to know that the shape of the response is right: every required field is present, every field has the correct type, and no surprises like strings where numbers should be. JSON Schema describes that shape in JSON itself:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"required": ["id", "status", "total", "items"],
"properties": {
"id": { "type": "string", "pattern": "^ord_" },
"status": { "enum": ["pending", "paid", "shipped", "cancelled"] },
"total": { "type": "number", "minimum": 0 },
"couponCode": { "type": ["string", "null"] },
"createdAt": { "type": "string", "format": "date-time" },
"items": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"required": ["sku", "qty", "price"],
"properties": {
"sku": { "type": "string" },
"qty": { "type": "integer", "minimum": 1 },
"price": { "type": "number", "minimum": 0 }
}
}
}
}
}
In a JavaScript test you can validate with Ajv:
import Ajv from 'ajv/dist/2020.js';
import addFormats from 'ajv-formats';
const ajv = new Ajv({ allErrors: true });
addFormats(ajv); // enables "format": "date-time", "email", etc.
const validate = ajv.compile(orderSchema);
const body = await res.json();
if (!validate(body)) {
throw new Error(`Schema errors: ${JSON.stringify(validate.errors, null, 2)}`);
}
Schema validation catches a whole class of contract bugs (renamed fields, changed types, missing required properties) in one assertion. Pair it with specific value assertions for the behaviour under test. Try the JSON Schema Validator with the schema above.
Comparing JSON
When you compare an expected and an actual JSON document, remember:
- Object key order does not matter; array order does.
- Some fields change every run (ids, timestamps). Either exclude them or assert their format rather than their value.
- Use partial matching for large responses: assert the fields you care about instead of the entire document, so unrelated additions do not break your test.
In Playwright and Jest-style assertions, toMatchObject does partial matching, and expect.any(String) matches a type rather than a value:
expect(body).toMatchObject({
status: 'paid',
customer: { email: 'ana@example.com' },
id: expect.any(String),
});
The JSON Diff tool shows structural differences between two documents when you are investigating a regression.
Practice exercise
Using the order JSON from this lesson:
- Write JSONPath expressions for: the customer id; all item quantities; the SKUs of items priced above 10.
- List five invalid variations of the payload that a
POST /ordersendpoint should reject, and name the expected status for each. - Add a rule to the schema so
currencyis required and must be a three-letter uppercase code.
Check your JSONPath in the JSONPath Tester and your schema in the JSON Schema Validator. A good answer uses filter expressions correctly, mixes syntax errors with semantic errors, and tests the schema against both a valid and an invalid currency.
Show a model answer
JSONPath
$.customer.idreturns"u_42"$.items[*].qtyreturns[2, 4]$.items[?(@.price > 10)].skureturns["BOOK-1"]
Invalid payloads
| Variation | Expected |
|---|---|
| Trailing comma after the last item | 400 (malformed JSON) |
"qty": 0 |
400/422 (minimum is 1) |
"qty": "2" (string) |
400/422 (wrong type) |
"items": [] |
400/422 (at least one item) |
"status": "refunded" on create |
400/422 (not an allowed value) |
Schema change: add "currency" to required, and under properties:
"currency": { "type": "string", "pattern": "^[A-Z]{3}$" }"USD" passes; "usd", "US" and "USDT" fail.
Key takeaways
- JSON has six types: string, number, boolean, null, object, array. There is no date type and no
undefined. - Syntax errors (single quotes, trailing commas, unquoted keys) should produce
400, never500. - Missing, null, empty and zero mean different things; test each deliberately, especially on PATCH.
- Use JSONPath (RFC 9535) or plain JavaScript to extract values, and JSON Schema to validate structure in one assertion.
- Compare JSON semantically: key order is irrelevant, volatile fields need format checks, and partial matching keeps tests focused.
Key terms
Finished this lesson?
Progress is saved in this browser. No account needed.
Practise it
Related interview questions
- What do you validate in an API response beyond the status code?
- Explain REST Assured's given/when/then structure and write a basic test
- How do you use JSON schema validation, filters and logging in a REST Assured framework?
- Which HTTP status codes matter in API testing? 401 vs 403, 400 vs 422, 200 vs 201 vs 204