JSON Path Assertions: Monitor What Your API Actually Returns
Learn how JSON path assertions catch broken APIs that return 200 OK with wrong data. Validate response bodies, fields, and arrays with confidence.
A health check that returns 200 OK tells you the server is alive. It tells you nothing about whether the payload it returned is correct. JSON path assertions close that gap by inspecting what your API actually sends back.
Why Status-Only Checks Miss Broken APIs
Most basic monitors stop at the HTTP status line. If the endpoint answers with a 2xx, they call it healthy and move on. The problem is that plenty of broken systems still answer with 200:
- A checkout endpoint returns
200but"total": nullbecause a downstream pricing service timed out. - A
/healthroute hardcodes{"status": "ok"}and never checks its database connection. - An auth API responds
200with an emptydataarray instead of the user object. - A cache layer serves a stale, malformed response that still parses as JSON.
In every case the status code lies. Your dashboards stay green while customers hit failures. This is exactly why we treat assertions as a core part of API monitoring best practices rather than an optional extra. Status codes are necessary but not sufficient — see HTTP status codes for monitoring for where they help and where they fall short.
What a JSON Path Is
A JSON path is an expression that addresses a specific value inside a JSON document. You navigate objects with dot notation and arrays with bracket notation:
data.user.email
items[0].id
order.lines[2].quantity
Reading left to right, you descend into nested keys (data → user → email) and index into arrays by position (items[0] is the first element). The path resolves to a single value you can then assert against.
| JSON path expression | What it selects |
|---|---|
status |
The top-level status field |
data.user.email |
The email nested under data.user |
items[0].id |
The id of the first array element |
items[-1].sku |
The sku of the last element (where supported) |
order.lines |
The entire lines array (for length checks) |
meta.flags.beta |
A deeply nested boolean flag |
Common Assertion Types
Once a path resolves to a value, you assert something about it. The most useful operators are:
- Equals — the value must match exactly (
statusequals"ok"). - Exists — the key must be present (regardless of value).
- Contains — a string or array must include a substring or element.
- Numeric comparison —
>,>=,<,<=for amounts, counts, and durations. - Array length — assert the number of elements (e.g. at least one result row).
- Type checks — the value must be a string, number, boolean, or object.
Real Examples on a Sample Payload
Consider a health endpoint that reports its dependencies:
{
"status": "ok",
"version": "2.14.0",
"dependencies": {
"database": "connected",
"cache": "connected",
"queue": "degraded"
},
"uptime_seconds": 84213
}
Useful assertions here:
statusequals"ok"dependencies.databaseequals"connected"dependencies.queueequals"connected"(this one would fire an alert)uptime_seconds>60
Now a checkout response, where correctness matters even more:
{
"order_id": "ord_91x2",
"currency": "USD",
"total": 4999,
"items": [
{ "sku": "PLAN-PRO", "qty": 1, "price": 4999 }
],
"payment": { "status": "captured" }
}
Strong assertions for this payload:
payment.statusequals"captured"total>0— catches thenull/0total bugitemsarray length>= 1— catches empty cartsitems[0].skuexistscurrencyequals"USD"
Together these turn a vague "is it up?" into a precise "is it returning a valid, paid order?" — the kind of signal that pairs naturally with uptime monitoring to give you a complete picture.
Pitfalls to Watch For
JSON path assertions are sharp tools, and a few edge cases trip people up:
Null vs. Missing
"total": null and an absent total key are different states. An exists assertion passes on null (the key is present) but fails on a missing key. If a null value is itself a bug, assert total > 0 or a type check instead of relying on existence alone.
Arrays Change Order
items[0] assumes a stable ordering. Many APIs do not guarantee element order, so indexing into position 0 can be flaky. Prefer asserting on array length, or on a value you expect to be present anywhere in the array, rather than pinning to a fragile index.
Type Coercion
Is "4999" (string) the same as 4999 (number)? In strict assertions, no. APIs sometimes serialize numbers as strings, or booleans as "true". Decide whether your equals check is type-sensitive and assert the type explicitly when it matters — a numeric comparison on a stringified number may silently fail.
Deeply Nested Optional Fields
A path like data.user.profile.address.city fails the moment any intermediate object is absent. Keep assertions shallow where you can, and assert the parent exists before drilling into leaves on responses where structure varies.
Putting It Together
The goal is to assert the smallest set of fields that proves the response is genuinely correct — status, a couple of critical values, and one structural check (array length or a key that must exist). That combination catches the silent 200 OK failures that status-only checks wave through.
A green check that doesn't read the response body is just a ping with extra steps. Assert the data, not the status line.