API Schema Validation
API schema validation checks a response against a schema: a machine-readable description of the structure the response must have — which fields exist and are required, what type each value has, which values and formats are allowed, and whether other fields may appear. For JSON APIs the schema is written in JSON Schema and usually sits inside the API's OpenAPI document.
For example, the order schema says total is a number. If the API returns "total": "149.50", a string, the validator reports a violation at $.total from the keyword type, although nobody wrote an assertion for that field. Where response validation checked fields one assertion at a time, a schema checks the whole structure of every response in one step.
Why it matters
Clients are built against the documented structure. A field that changes type, disappears, turns null or is renamed can crash a mobile app or silently corrupt a report while the status is still 200 OK. Such defects are easy to miss by eye and often hit many endpoints at once, for example after a change in how the server serialises its data, that is, turns its objects into JSON.
For the tester, a schema is a partial test oracle — a source of expected results — that comes with the API description. One check covers dozens of fields, catches undocumented fields that may expose internal data, and makes every regression run a structural check. Reviewing a schema also forces the right questions early: optional or nullable, which values, may the API add fields?
Reading a JSON Schema: the keywords that do most of the work
A JSON Schema is itself a JSON document. Each keyword is a rule that a validator applies to the matching part of the response. $schema names the draft the schema follows — the current one is 2020-12 — because validators behave slightly differently between drafts.
The schema below describes the body of GET /orders/{orderId}, built from the keywords most schemas use:
type:object,array,string,number,integer,booleanornull. A list such as["string", "null"]allows either.required: the fields that must be present; a field listed only inpropertiesis optional. It checks presence, not value:"couponCode": nullis present.properties: the rule for each named field, applied when the field is there.itemsdoes the same for every element of an array.enum,pattern,minimum,maxLengthand similar: allowed values, text patterns and limits.enumis an exact, case-sensitive match.patternis a regular expression:^ORD-[0-9]+$means "starts withORD-, then one or more digits, then ends". Without^and$it may match anywhere in the text, sox-ORD-1-ywould pass.format: a named format such asdate-timeoremail. Validators may treat it as an annotation only, the default in draft 2020-12, so check that yours really rejects a bad value.additionalProperties: whether fields not listed inpropertiesmay appear. Without it, any extra field is accepted;falserejects them.
required asks only for presence, additionalProperties: false rejects undocumented fields, and format counts only if the validator asserts it.JSON Schema for GET /orders/{orderId}
{ Part 1:"$schema": "https://json-schema.org/draft/2020-12/schema", Part 2:"type": "object", Part 3:"required": ["id", "status", "total", "createdAt", "items"], Part 4:"properties": { "id": { "type": "string", Part 5:"pattern": "^ORD-[0-9]+$" }, "status": { Part 6:"enum": ["pending", "paid", "shipped", "cancelled"] }, "total": { "type": "number", "minimum": 0 }, "createdAt": { "type": "string", Part 7:"format": "date-time" }, "couponCode": { Part 8:"type": ["string", "null"] }, "items": { "type": "array", Part 9:"minItems": 1, "items": { "type": "object", "required": ["sku", "qty", "price"], "properties": { "sku": { "type": "string" }, "qty": { "type": "integer", "minimum": 1 }, "price": { "type": "number", "minimum": 0 } } } } }, Part 10:"additionalProperties": false }
- 1
$schema: the draft, here 2020-12Validators differ slightly between drafts - 2
type: the body must be a JSON objectAn array, a string ornullat the top fails - 3
required: these five fields must be presentPresence only: a field set tonullis present - 4
properties: one rule per named fieldApplied only when the field is there - 5
pattern:idmust match the regular expressionNot anchored unless it starts with^and ends with$ - 6
enum: only these four status valuesExact and case-sensitive:"Shipped"fails - 7
format: an RFC 3339 date and timeAn annotation by default in 2020-12: check that your validator asserts it - 8Optional and nullable:
couponCodeNot inrequired, andnullis an allowed type - 9
minItems: at least one line itemWithout it, an empty list passes everyitemsrule - 10
additionalProperties: no undocumented fieldsWithout it, any extra field is accepted
Text version
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"required": ["id", "status", "total", "createdAt", "items"],
"properties": {
"id": { "type": "string", "pattern": "^ORD-[0-9]+$" },
"status": { "enum": ["pending", "paid", "shipped", "cancelled"] },
"total": { "type": "number", "minimum": 0 },
"createdAt": { "type": "string", "format": "date-time" },
"couponCode": { "type": ["string", "null"] },
"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 }
}
}
}
},
"additionalProperties": false
}Where the schema lives: OpenAPI
An OpenAPI document, written in YAML or JSON, describes a whole HTTP API: paths, methods, parameters, request bodies and, for every status code, the response body per media type. Bodies are described with schemas, often defined once under components/schemas and reused with $ref. A validator picks the schema by path, method, status code and media type, so a 404 is checked against the error schema, not the order schema.
Versions matter. OpenAPI 3.1 and 3.2 use JSON Schema 2020-12, plus a few OpenAPI-only keywords. OpenAPI 3.0 uses an extended subset of an older draft: type takes a single value, and a nullable field is written nullable: true instead of a type list. Check the openapi line at the top before you trust a validator's results.
openapi: 3.1.0
info: { title: Orders API, version: 1.0.0 }
paths:
/orders/{orderId}:
get:
parameters:
- { name: orderId, in: path, required: true, schema: { type: string } }
responses:
'200':
description: The order
content:
application/json:
schema:
$ref: '#/components/schemas/Order'
'404':
description: No order with this id
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
components:
schemas:
Order:
type: object
required: [id, status, total, createdAt, items]
additionalProperties: false
properties:
id: { type: string, pattern: '^ORD-[0-9]+$' }
status: { enum: [pending, paid, shipped, cancelled] }
couponCode: { type: [string, 'null'] }
# ...the other properties, exactly as in the JSON Schema above
Error:
type: object
required: [code, message]
properties:
code: { type: string }
message: { type: string }Worked example: an order that returns 200 OK
A tester calls GET /orders/ORD-1042 and receives 200 OK with the body shown at the end of this section. The status check passes and nothing looks broken at a glance.
Validated against the order schema, the body gives five violations, each a location and the keyword that rejected it, and one pass worth noticing:
$.status,enum:"Shipped"is not allowed. The documented value isshipped, and a client that compares strings exactly will not recognise it.$.total,type: a string where a number is required. A client that adds up totals may join the text or fail.$.createdAt,format: not an RFC 3339 date-time — noTseparator, no seconds, no offset. Reported only if the validator asserts formats.$.items[0],required: the line item has noprice.$.internalNote,additionalProperties: a field the contract does not allow, here internal data. The OWASP API Security Top 10 (2023) lists exposing such properties under API3, Broken Object Property Level Authorization.couponCodeisnulland passes: it is optional and its type allowsnull.
HTTP/1.1 200 OK
Content-Type: application/json
{
"id": "ORD-1042",
"status": "Shipped",
"total": "149.50",
"createdAt": "2026-09-14 10:32",
"couponCode": null,
"items": [
{ "sku": "BK-201", "qty": 2 }
],
"internalNote": "VIP customer, call before delivery"
}What schema validation cannot tell you
A schema describes the shape of valid data, not whether the data is right. Fix every violation above and the response can still be wrong: with "total": 159.50 and one line of two items at 74.75, the schema passes although the items add up to 149.50. A passing check means only that the response is structurally valid against that version of the document.
These need other checks:
- Values and calculations: totals, counts, and whether
idis the order you asked for. JSON Schema cannot compare two values or calculate. - Business rules and state: whether
shippedis a valid next status for this order.if/thencan express simple conditions, but not the order's history. - Authorisation: whether this caller may see this order at all.
- Status codes, headers and timing: a body validator does not see them; assert them separately.
- Empty arrays: an empty
itemslist passes anyitemsrule, so a list endpoint returning[]never exercises the item schema. UseminItemswhere the contract guarantees content, and test data that produces non-empty lists. - The schema itself: if the document is wrong or out of date, the validator agrees with it. A schema generated from one sample usually marks every field it saw as required and copies the sample's defects; review it against the requirements first.
Common mistakes
- Reading a passing schema check as "the response is correct". It proves the structure only; values still need assertions.
- Assuming a field in
propertiesis mandatory. Onlyrequiredmakes it so, andrequiredacceptsnull; usetypeto forbid it. - Leaving out
additionalPropertiesand missing leaked fields — or setting it tofalsein a client's checks without agreeing with the team, so every new optional field breaks the build. - Trusting
formatwithout checking that the validator asserts it:"email": "not-an-email"may pass silently. - Validating against a schema generated from one sample, or copied into the test months ago, instead of the current OpenAPI document.
- Validating only success responses. Error responses (
400,404,422) have schemas too, and clients parse them.
The QA perspective
Schema work starts before any response exists. Review the draft OpenAPI document as a test basis and ask of every field: is it required, can it be null, which values and formats are allowed, what are its limits, are extra fields acceptable? Ambiguities found here are cheap to fix and become the boundary and negative tests of later lessons.
During execution, run a schema check on every response in your API tests and add assertions for the values it cannot see. When a check fails, record the request, location, keyword, expected rule and the OpenAPI version you used. Then ask the developer or API owner which is wrong — the API or the document — because fixing the wrong one hides the defect.
Practice exercise
Review a product schema and validate a response
Requirement: GET /products/{sku} returns sku (text), name (1 to 120 characters), price in SAR (greater than 0) and stock (a whole number, 0 or more), all always present, plus an optional tags list of text labels. No other fields are allowed. A colleague wrote the schema below; the API returned the response under it.
Find where the schema misses the requirement, then work out what each version of the schema reports.
Schema (colleague's draft):
{
"type": "object",
"required": ["sku", "name"],
"properties": {
"sku": { "type": "string" },
"name": { "type": "string", "maxLength": 120 },
"price": { "type": "number", "minimum": 0 },
"stock": { "type": "number" },
"tags": { "type": "array", "items": { "type": "string" } }
}
}
Response body:
{ "sku": "BK-201", "name": "", "stock": 2.5, "tags": ["new", 7], "costPrice": 41.2 }- Compare the schema with the requirement keyword by keyword; list every gap and its fix.
- Paste the schema and the response into the JSON Schema Validator and note what it reports.
- Correct the schema, validate again and list each violation with its location and keyword.
- Name two things the requirement implies that no schema can check.
Show the model answer
Gaps: required must also list price and stock; name needs "minLength": 1; price needs "exclusiveMinimum": 0, because "minimum": 0 accepts 0; stock must be "type": "integer" with "minimum": 0; and "additionalProperties": false is missing.
The colleague's schema reports one violation: $.tags[1], type (7 is not a string). The empty name, missing price, fractional stock and extra costPrice all pass.
The corrected schema reports five: $, required (no price); $.name, minLength; $.stock, type; $.tags[1], type; $.costPrice, additionalProperties. The last deserves its own bug report: a cost price is internal commercial data.
No schema can check that the returned sku is the one requested, or that price and stock match the catalogue and the warehouse. Those need value assertions against a known source of truth.
Frequently asked questions
What is the difference between JSON Schema and OpenAPI?
JSON Schema describes one JSON value: its fields, types, formats and limits. OpenAPI describes a whole HTTP API — paths, methods, parameters, status codes, headers and security — and uses schemas for the bodies. OpenAPI 3.1 and 3.2 schemas are JSON Schema 2020-12 plus a few OpenAPI keywords; OpenAPI 3.0 uses an extended subset of an older draft.
Is schema validation the same as contract testing?
It is one part of it. Checking responses against the provider's OpenAPI document is specification-based contract testing, and some teams call any schema check a contract test. Contract testing also covers consumer-driven contracts, which a later lesson explains.
Should response schemas set additionalProperties to false?
In the provider's own tests it is often worth it, because it catches undocumented and leaked fields. A client, on the other hand, should usually tolerate new fields, or every additive change breaks it. Agree on the rule with the developers and record it in your test approach, so a failure means the same thing to everyone.
Can a schema check rules between fields?
Partly. dependentRequired can say "if refundId is present, refundedAt must be too", and if/then/else can say "when status is cancelled, cancelledAt is required". A schema cannot compare two values or calculate, so "total equals the sum of the items" or "end date after start date" stay as value assertions.
References
- JSON Schema SpecificationThe vocabulary for describing and validating the shape of JSON documents (types, required properties, formats).
- OpenAPI SpecificationThe standard for describing REST APIs: paths, operations, parameters, schemas, responses and security schemes.
- ISTQB GlossaryThe standard definitions of testing terms — test case, regression testing, severity, priority, boundary value analysis and the rest.
- OWASP API Security Top 10 (2023)The ten most common API weaknesses: broken object-level authorization, broken authentication, unrestricted resource consumption and more.
Practise with Mutqan tools
Validate a JSON document against a JSON Schema (draft-07 / 2019-09 / 2020-12 core keywords).
Validate API responses against a JSON Schema or an OpenAPI response schema.
Render an OpenAPI 3.x / Swagger 2.0 spec (JSON or YAML) as browsable endpoint documentation.
Validate OpenAPI 3.x and Swagger 2.0 documents for structural errors and best-practice issues.
Generate a JSON Schema from a sample JSON document automatically.
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.