API Response Validation
API response validation is the part of an API test that decides whether a response is correct. You compare the actual response with the expected result, layer by layer: the status code, the headers, which body fields are present, their types, their values, the business rules that connect them, and how long the response took. Each comparison is an assertion: one precise check that passes or fails.
Take POST /api/v1/orders, which creates an order. "It returned something" is not a test. "The status is 201, Content-Type is application/json, $.status is "pending" and $.total equals the sum of quantity times unit price" is. The $.total notation is JSONPath, a small query language for addressing values inside a JSON document.
Why it matters
APIs fail in ways a quick look does not reveal. A 200 OK can carry an error message in its body, a number can quietly become a string, a total can leave out an item, and a profile response can include a password hash. Every client of the API, whether a web app, a mobile app or a partner integration, inherits these defects, often without noticing until something breaks.
Validation also decides what a test is worth. A test that checks only the status code passes whenever the server answers, which gives false confidence. Precise assertions turn each run into evidence: a failed assertion names the field, the expected value and the actual value, which is most of a good bug report.
Seven layers of a response check
Read a response from the outside in. The status line says what happened, the headers describe the message and the body carries the data. Inside the body, check structure first (are the fields there?), then types, then values, and finally the rules that connect several values. Timing is not written in the response; your client measures it, but it belongs to the same check.
Every assertion needs a test oracle, the source of the expected result: the API documentation, the acceptance criteria or a business rule, never the response itself. The example in this lesson is the response to a request that created an order for customer 7 with two items.
- Status code: exactly the documented code. A created resource returns
201, not "any 2xx". Assert the number; the reason phrase ("Created") is informational only, and HTTP/2 does not send one. - Headers:
Content-Typegives the media type of the body, hereapplication/json; after a201, aLocationheader identifies the new resource. RFC 9110 makes header names case-insensitive, so compare them that way. - Fields present: every required field exists, and fields that must never be sent, such as
password, are absent. - Types:
idandcustomerIdare integers,totalis a number anditemsan array."7"in quotes would be a string: a type defect even though the value looks right. - Values: what you sent comes back unchanged (
customerIdis 7), enumerations are in the allowed set (statusispending), and formats are right (createdAtis an RFC 3339 timestamp ending inZor an offset). - Business rules: values agree with each other.
totalis 2 × 99.95 + 1 × 50.00 = 249.90, and the number at the end ofLocationequalsid. - Timing: the response arrives within the budget the team agreed for this endpoint. Here the client measured 184 ms against a budget of 1,000 ms in the test environment.
201 response, seven layers of checks: status code, headers, required and forbidden fields, types, values, a business rule linking total to the items, and the time the client measured.Response
HTTP/1.1 Part 1:201 Created Part 2:Content-Type: application/json Location: /api/v1/orders/9021 { Part 3:"id": 9021, Part 4:"customerId": 7, Part 5:"status": "pending", "items": [ { "sku": "A-100", "qty": 2, "unitPrice": 99.95 }, { "sku": "B-200", "qty": 1, "unitPrice": 50.00 } ], Part 6:"total": 249.90, "createdAt": "2026-10-01T09:30:12Z" }
Measured by the client, not part of the message
response time: Part 7:184 ms
- 1Status codeExactly
201: the order was created. The reason phrase is not asserted.status == 201Pass - 2HeadersMedia type
application/json;Locationends in the new order’sid.Location == /api/v1/orders/9021Pass - 3Fields presentRequired fields exist, and a field that must never be sent is absent:
$.password → (no match)Pass - 4TypesThe integer 7, not the string
"7";totalis a number anditemsan array.$.customerId type integerPass - 5ValuesA new order is
pending, andcustomerIdis the 7 you sent.$.status == "pending"Pass - 6Business rule
totalis the sum ofqty × unitPriceover the items.2 × 99.95 + 1 × 50.00 = 249.90Pass - 7TimingWithin the budget the team agreed for this endpoint: 1,000 ms.
184 ms <= 1000 msPass
Text version
HTTP/1.1 201 Created
Content-Type: application/json
Location: /api/v1/orders/9021
{
"id": 9021,
"customerId": 7,
"status": "pending",
"items": [
{ "sku": "A-100", "qty": 2, "unitPrice": 99.95 },
{ "sku": "B-200", "qty": 1, "unitPrice": 50.00 }
],
"total": 249.90,
"createdAt": "2026-10-01T09:30:12Z"
}Pointing at fields with JSONPath
An assertion needs an address for the value it checks. JSONPath, standardised by the IETF as RFC 9535, writes that address as an expression that starts at $, the root of the document, and moves down one segment at a time: .name for an object member, [0] for an array index, [*] for every element, .. for any depth and [?(…)] for a filter, in which @ stands for the element being tested. RFC 9535 also accepts a filter without the parentheses ([?@.qty>1]), but many tools written before the standard require them, so this lesson writes them. Such tools differ in other details too, so try an expression in your own tool first.
The result is a nodelist: zero, one or many values. An empty result is not an error; it means nothing matched. That makes absence easy to assert and typos dangerous: $.totl matches nothing, so an assertion on it must fail, not pass silently. Results are parsed values, so 50.00 in the text is the number 50.
- One value:
$.statusmust equal"pending". - Absence:
$.passwordmust return nothing. Sensitive or internal fields are caught only by asserting that they are missing. - Every element: select the elements that break the rule and expect an empty result. If
$.items[?(@.qty<1)]returns nothing, no item has a numeric quantity below 1. A comparison skips an item whoseqtyis missing or is a string, so pair the rule with presence and type checks. This works in any tool, including tools that compare a value rule with the first match only. - A count: when you expect two items, assert the length of
$.itemsinstead of checking only the first element.
Expression Result on the example body
$.status "pending"
$.items[0].sku "A-100"
$.items[-1].unitPrice 50 last element
$.items[*].qty 2, 1 every element
$..sku "A-100", "B-200" any depth
$.items[?(@.qty>1)].sku "A-100" filter
$.password (no match) absent, as requiredWorked example: the checklist for a new order
The documentation says: "POST /api/v1/orders creates an order and returns 201 Created, a Location header with the order's URL and the order as JSON. A new order has the status pending. total is the sum of qty × unitPrice over all items."
The request sent customerId 7 and two items, A-100 × 2 and B-200 × 1. Writing one assertion per line, each with its layer and its oracle, gives a checklist you can run by hand, paste into a validator or automate later.
- Values that change on every run,
idandcreatedAt, are checked by type, format and relationship (theLocationnumber equalsid;createdAtis close to the time of the request), never by an exact value. - Values you control are checked exactly: you sent customer 7 and two items, so you expect them back.
- The expected total is computed with the documented rule from the items in the response: the rule, not the response, is the oracle. Compare numbers as numbers:
249.90and249.9are different text but the same JSON number. - One check reaches beyond this response: a
GETon theLocationURL should return the same order, which shows that it was stored, not only echoed.
# Layer Assertion Oracle
1 status status == 201 documentation
2 headers Content-Type contains application/json documentation
3 headers Location == /api/v1/orders/{id} documentation
4 fields $.id, $.status, $.items, $.total exist documentation
5 fields $.password does not exist security rule
6 types $.id integer, $.total number, $.items array documentation
7 values $.customerId == 7, $.items length == 2 the request
8 values $.status == "pending" business rule
9 values $.createdAt is RFC 3339 with Z or an offset documentation
10 rule $.total == sum of qty * unitPrice (249.90) business rule
11 timing response time <= 1000 ms (measured: 184 ms) team budgetWhat to assert in every response, and what not to
Not every endpoint needs eleven checks, but every response deserves a minimum, error responses included: an error response gets the exact documented 4xx code, a JSON content type and an error body with the documented shape.
- Always: the exact status code, the
Content-Typemedia type, a body that parses as JSON, the required fields and the absence of forbidden ones. A204 No Contentresponse has no body, so for it you assert only the status and the headers. - Data you sent or can predict: exact values. Generated data such as ids, timestamps and tokens: type, format, range and relationships.
- Business rules: at least one assertion for each rule the endpoint implements.
- Timing: a budget check on every call. One slow sample is a reason to measure again, not yet a performance defect.
- Not significant, so do not assert it: the order of members in a JSON object (RFC 8259 defines an object as unordered), the order of headers with different names, the letter case of header names and whitespace in the body.
- Array order: assert it only when the API documents a sort order, and then assert it explicitly.
Common mistakes
- Asserting only the status code. A
200 OKwith{"error": "out of stock"}in the body passes such a test. - Copying the current response into the expected result. If the response is already wrong, the test now protects the defect; take expected values from the oracle.
- Asserting exact values for generated data such as ids and timestamps: the test then fails on the next run although nothing is wrong.
- Treating an empty JSONPath result as a pass. A typo such as
$.totlmatches nothing, and an assertion on it must fail. - Comparing numbers as text, so
249.9fails against249.90, or comparing loosely, so the string"249.90"passes where a number was required. - Checking one array element and assuming the rest: the first item is correct, the third has a total that is a string.
The QA perspective
On a team, response validation starts before the API exists. When you review the API documentation or the OpenAPI description, ask for every response: what is the exact status, which fields are required, which may be null, what values and formats are allowed, and which rule connects them? A missing answer is a documentation defect, and it is cheapest to fix at that point.
When you execute, keep the checklist with the test case so another tester can repeat it, and record the actual value of every failed assertion. A good API bug report gives the request, the expected and actual value of the failed check, and the oracle: "$.items[2].total is the string "75.50"; the documentation defines total as a number." Agree with the developers which checks will be automated, so that manual effort goes to the rules and questions a script cannot ask.
Practice exercise
Validate a filtered order list
The documentation says: "GET /api/v1/orders?status=paid returns 200 and application/json. The body has page, pageSize and items; items holds at most pageSize orders, each with the status paid, sorted by createdAt, newest first. total is a number. Target response time: 1,000 ms." Your client received the response below in 2,340 ms.
Write the checklist for this response, then decide which checks pass and which fail. Use pen and paper, or paste the response into the API Response Validator and the JSON Path Tester.
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
{
"page": 1,
"pageSize": 20,
"items": [
{ "id": 9021, "status": "paid", "total": 249.90,
"createdAt": "2026-10-01T09:30:12Z" },
{ "id": 9017, "status": "pending", "total": 120.00,
"createdAt": "2026-09-30T18:05:44Z" },
{ "id": 9010, "status": "paid", "total": "75.50",
"createdAt": "2026-09-29T11:20:03Z" }
]
}- List the checks layer by layer: status, headers, fields, types, values, business rules, timing.
- Write each body check as a JSONPath expression with its expected result; for "every item" rules, select the items that break the rule.
- Run the checks and record pass or fail with the actual value.
- Write a one-line bug title for each failure.
Show the model answer
Pass: status 200; $.page is 1; $.items is an array of 3 elements, so not more than pageSize (20); the createdAt values are 1 October, 30 September and 29 September, so newest first holds; every timestamp ends in Z.
Fail, headers: Content-Type is text/html; charset=utf-8 while the body is JSON. Bug title: "GET /api/v1/orders returns JSON labelled as text/html".
Fail, business rule: $.items[?(@.status!='paid')].id returns 9017, but the result must be empty. Bug title: "status=paid filter returns pending order 9017".
Fail, types: $.items[2].total is the string "75.50", not a number. Checking the type of every element by hand is slow; schema validation checks all elements at once. Bug title: "Order 9010 returns total as a string".
Warning, timing: 2,340 ms is over the 1,000 ms target. One sample is not enough; repeat the request a few times and report it if the time stays above the target.
Frequently asked questions
What is the difference between JSONPath and JSON Pointer?
JSON Pointer (RFC 6901) identifies exactly one value by its path, such as /items/0/sku. JSONPath (RFC 9535) is a query: wildcards, filters and descendants can select zero, one or many values, such as $.items[*].sku. Response validators such as the ones in this lesson use JSONPath because assertions often concern several values or their absence.
Is checking the status code not enough for a smoke test?
For a quick "is the service up" check, the status code and the content type can be enough. For a test that claims a feature works, they are not: the status code says nothing about the body, and a 200 can carry a wrong total or an error message. Add at least the required fields and one business rule.
Should I validate responses manually or with automation?
Both, at different times. Design and the first run are manual: you read the documentation, choose the oracle for each check and look at the real response. Once the checklist is stable, the same assertions become automated checks that run on every build, and your manual time goes to new endpoints and exploratory questions.
How is this different from schema validation?
A schema describes the structure that every valid response shares: required fields, types, formats and allowed values. It does not calculate, so it cannot check that total equals the sum of the items, and it does not know your request, so it cannot check that the filter you asked for was applied or that a value is the one you sent. Use the schema for shape and assertions for meaning.
References
- RFC 9110: HTTP SemanticsThe meaning of HTTP methods, header fields, status codes and content negotiation, independent of the wire version.
- RFC 9535: JSONPath: Query Expressions for JSONThe standard JSONPath syntax — root, child, descendant, wildcard and filter selectors — and how a query is evaluated.
- RFC 8259: The JavaScript Object Notation (JSON) Data Interchange FormatThe JSON grammar: objects, arrays, strings, numbers, the three literals, and UTF-8 as the interchange encoding.
- ISTQB GlossaryThe standard definitions of testing terms — test case, regression testing, severity, priority, boundary value analysis and the rest.
Practise with Mutqan tools
Validate an API response against expectations: status, required headers, JSON schema and field rules.
Evaluate JSONPath expressions ($.store.book[*].author) against JSON and see matches live.
Test REST endpoints with full control over method, headers and JSON body, then assert on status, headers and JSON paths.
Explore JSON as a collapsible tree with paths, types and value counts.
Analyze an API response: structure, field types, nulls, duplicates, sizes and potential data-quality issues.
Go deeper
Your progress on this topic
Not started
Mark the topic complete when you have studied it and done the exercise.
Sign in to track your progress
Mark topics complete, continue where you left off and unlock specialisation exams.