To validate an API response against your OpenAPI schema, take the operation's responses object, pick the entry for the status code you received, read the schema under content and application/json, and run the response body through a JSON Schema validator. Fail the check when a required property is missing, a value has the wrong type, an enum value is not in the list, or the body contains properties the schema forbids. This is a different job from validating the spec file. A spec can be perfectly valid while the server returns something else entirely.
Why a valid spec is not enough
Linting a spec checks that the document follows the OpenAPI rules: paths are well formed, $ref pointers resolve, schemas use known keywords. It says nothing about the running service. The gap between the two is called contract drift, and it opens quietly. A backend developer renames a field during a refactor, an ORM starts serializing integers as strings, or a new column leaks into a response because the serializer returns the whole database row. Clients generated from the spec then break at runtime, usually in production, because nothing compared the response to the API contract.
Response validation closes that gap. It is a form of schema validation applied to real traffic: the spec says what the body must look like, and the check confirms that it does.
Match the status code to its response entry
An operation can declare a different schema for every status code, so picking the right one is the first step. The OpenAPI Specification defines the lookup order. Use the exact code if the spec lists it, such as "200" or "404". If not, use a range key such as "2XX" or "4XX" (the spec writes ranges with an uppercase X). If neither exists, fall back to "default". If none of these match, the response is undocumented, which is a finding in itself: the API returned a status the contract never mentioned.
- Read content, then the media type. Most JSON APIs use application/json; some use vendor types such as application/problem+json for errors.
- Resolve every $ref before validating, so the validator sees the full schema instead of a pointer into components.
- Know your OpenAPI version. OpenAPI 3.1 schemas are JSON Schema 2020-12. OpenAPI 3.0 uses an extended subset with its own nullable keyword, which some validators need to be told about.
A worked example: one response, four problems
Here is a small users API. The 200 response for GET /v1/users/{userId} requires id, email, createdAt and plan, restricts plan to three values, types seats as an integer, and sets additionalProperties to false so nothing undeclared may appear. APIForge's design score for this spec is 91 out of 100, so the document itself is in good shape.
openapi: 3.0.3
info:
title: Users API
version: 1.0.0
paths:
/v1/users/{userId}:
get:
summary: Get a user by ID
description: Returns one user account.
parameters:
- name: userId
in: path
required: true
schema:
type: string
responses:
"200":
description: The user
content:
application/json:
schema:
type: object
additionalProperties: false
required: [id, email, createdAt, plan]
properties:
id:
type: string
email:
type: string
createdAt:
type: string
plan:
type: string
enum: [free, pro, team]
seats:
type: integer
"404":
description: User not foundNow the server returns this body after a refactor. It looks reasonable at a glance, which is how drift gets past code review.
{
"id": "usr_8f2k",
"email": "dana@example.com",
"created_at": "2026-09-14T10:22:00Z",
"plan": "enterprise",
"seats": "5",
"lastLoginIp": "203.0.113.7"
}We ran this body through APIForge's response validator. It reported four distinct problems:
| Path | Finding | Likely cause |
|---|---|---|
| $.createdAt | Missing required property | The serializer switched to snake_case |
| $.created_at | Property not declared in the schema | Same rename, seen from the other side |
| $.seats | Expected integer, got string | A database driver or a format helper turned the number into text |
| $.lastLoginIp | Property not declared in the schema | The whole user row is being returned, including a personal data field |
The last finding is the serious one. An IP address in a public response is a data exposure, and additionalProperties: false is what made it visible. There is also a fifth problem the quick check did not flag: "enterprise" is not one of the allowed plan values. APIForge's in-browser check covers types, required fields, undeclared properties and casing drift; it does not evaluate enum, format, pattern, or combinators such as oneOf and allOf. A full JSON Schema validator such as Ajv reports the enum violation, as the next section shows.
Validate responses in code with Ajv
For automated checks, dereference the spec, pull out the schema, compile it once with Ajv, and validate each response body. The script below fails the process when the body does not match, so it works as a CI step against a staging environment.
import SwaggerParser from "@apidevtools/swagger-parser";
import Ajv from "ajv";
import addFormats from "ajv-formats";
const api = await SwaggerParser.dereference("./openapi.yaml");
const schema =
api.paths["/v1/users/{userId}"].get.responses["200"].content["application/json"].schema;
const ajv = new Ajv({ allErrors: true, strict: false });
addFormats(ajv);
const validate = ajv.compile(schema);
const res = await fetch("https://staging.example.com/v1/users/usr_8f2k");
const body = await res.json();
if (res.status !== 200 || !validate(body)) {
console.error(res.status, validate.errors);
process.exitCode = 1;
}Set strict: false so Ajv does not reject OpenAPI-only keywords such as example or xml. Ajv supports the OpenAPI 3.0 nullable keyword without extra options, but nullable does not add null to an enum list; include null in the enum itself if it is allowed. Also remember that in JSON Schema, format is an annotation by default. Without ajv-formats, a string like "not-an-email" passes format: email.
Ajv reports each failure as an object with instancePath, keyword and message. For the body above it reports instancePath "/seats" with keyword "type" and the message "must be integer", and instancePath "/plan" with keyword "enum". A failing required check has an empty instancePath and names the missing property in params.missingProperty. Print instancePath and message side by side in your CI log, and a reviewer can see which field broke without opening the schema.
Where to run response validation
Three places, three different jobs
Contract tests in CI
Run requests against a staging deploy and fail the build on any mismatch. This catches drift before release. Cover every documented status code, not only 200, including the error schemas described in REST API error handling.
During development
Validate responses while you build the endpoint, so the spec and the code change together. This is where a quick browser check is fastest.
Sampled production monitoring
Validate a small sample of real responses and log mismatches without blocking traffic. Production data finds the edge cases test fixtures never contain, such as null values in old rows.
Choose how strict to be
Not every mismatch should break a build. A sensible split is strict checks on the server side, where you own the contract, and tolerant parsing on the client side, where an extra field should never crash an app. The table shows one way to set the rules.
| Rule | CI contract test | Production monitoring |
|---|---|---|
| Missing required property | Fail | Alert |
| Wrong type | Fail | Alert |
| Enum value not in list | Fail | Alert |
| Undeclared property | Fail if additionalProperties is false, otherwise warn | Log |
| Undocumented status code | Fail | Log |
Whether to set additionalProperties: false is a design choice. It turns any new response field into a contract change, which some teams find too rigid; the API versioning guide explains why adding a field is normally non-breaking. A middle ground is to keep additionalProperties open in the published spec but validate against a strict copy in your own tests, so leaks like lastLoginIp still fail the build. The response schema best practices cover how to model these schemas, and the RFC 7807 error response example shows a strict error schema.
Check a response in APIForge
In the API testing workbench, load your spec, pick an operation, and send the request. APIForge tries the request from your browser first and falls back to its server proxy when CORS blocks it. After the response arrives, run Validate response: it finds the schema for the returned status code, lists missing fields, type mismatches, undeclared properties and casing drift with their JSON paths, and can explain each issue in plain language. Use it while you build and debug; use Ajv or a similar full validator for the CI gate.