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-Type gives the media type of the body, here application/json; after a 201, a Location header 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: id and customerId are integers, total is a number and items an array. "7" in quotes would be a string: a type defect even though the value looks right.
  • Values: what you sent comes back unchanged (customerId is 7), enumerations are in the allowed set (status is pending), and formats are right (createdAt is an RFC 3339 timestamp ending in Z or an offset).
  • Business rules: values agree with each other. total is 2 × 99.95 + 1 × 50.00 = 249.90, and the number at the end of Location equals id.
  • 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.
One 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
  1. 1Status codeExactly 201: the order was created. The reason phrase is not asserted.status == 201Pass
  2. 2HeadersMedia type application/json; Location ends in the new order’s id.Location == /api/v1/orders/9021Pass
  3. 3Fields presentRequired fields exist, and a field that must never be sent is absent:$.password → (no match)Pass
  4. 4TypesThe integer 7, not the string "7"; total is a number and items an array.$.customerId type integerPass
  5. 5ValuesA new order is pending, and customerId is the 7 you sent.$.status == "pending"Pass
  6. 6Business ruletotal is the sum of qty × unitPrice over the items.2 × 99.95 + 1 × 50.00 = 249.90Pass
  7. 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"
}

Try it: API Response Validator

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: $.status must equal "pending".
  • Absence: $.password must 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 whose qty is 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 $.items instead 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 required

Try it: JSON Path Tester Try it: JSON Tree Viewer

Worked 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, id and createdAt, are checked by type, format and relationship (the Location number equals id; createdAt is 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.90 and 249.9 are different text but the same JSON number.
  • One check reaches beyond this response: a GET on the Location URL 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 budget

Try it: REST API Tester

What 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-Type media type, a body that parses as JSON, the required fields and the absence of forbidden ones. A 204 No Content response 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.

Try it: API Response Analyzer

Common mistakes

  • Asserting only the status code. A 200 OK with {"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 $.totl matches nothing, and an assertion on it must fail.
  • Comparing numbers as text, so 249.9 fails against 249.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

Your task

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.

Material
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" }
  ]
}
Steps
  1. List the checks layer by layer: status, headers, fields, types, values, business rules, timing.
  2. Write each body check as a JSONPath expression with its expected result; for "every item" rules, select the items that break the rule.
  3. Run the checks and record pass or fail with the actual value.
  4. Write a one-line bug title for each failure.
Show the model answer
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

Practise with Mutqan tools

Validate an API response against expectations: status, required headers, JSON schema and field rules.

API Testing

Evaluate JSONPath expressions ($.store.book[*].author) against JSON and see matches live.

JSON & Data

Test REST endpoints with full control over method, headers and JSON body, then assert on status, headers and JSON paths.

API Testing

Explore JSON as a collapsible tree with paths, types and value counts.

JSON & Data

Analyze an API response: structure, field types, nulls, duplicates, sizes and potential data-quality issues.

API Testing

Go deeper

Your progress on this topic

Part of the QA Roadmap