Validate API Responses Against Your OpenAPI Schema

Check live API responses against the OpenAPI response schema: find the right schema, catch missing fields and type drift, and run checks with Ajv in CI.

On this page

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.

YAMLopenapi.yaml
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 found

Now the server returns this body after a refactor. It looks reasonable at a glance, which is how drift gets past code review.

JSON
{
  "id": "usr_8f2k",
  "email": "dana@example.com",
  "created_at": "2026-09-14T10:22:00Z",
  "plan": "enterprise",
  "seats": "5",
  "lastLoginIp": "203.0.113.7"
}
The response body under test.

We ran this body through APIForge's response validator. It reported four distinct problems:

PathFindingLikely cause
$.createdAtMissing required propertyThe serializer switched to snake_case
$.created_atProperty not declared in the schemaSame rename, seen from the other side
$.seatsExpected integer, got stringA database driver or a format helper turned the number into text
$.lastLoginIpProperty not declared in the schemaThe whole user row is being returned, including a personal data field
Output of the APIForge response check, grouped by property.

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.

JavaScriptcheck-user-response.mjs
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;
}
allErrors reports every problem instead of stopping at the first. For OpenAPI 3.1 specs, import Ajv from "ajv/dist/2020" so it uses JSON Schema 2020-12.

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

  1. 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.

  2. 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.

  3. 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.

RuleCI contract testProduction monitoring
Missing required propertyFailAlert
Wrong typeFailAlert
Enum value not in listFailAlert
Undeclared propertyFail if additionalProperties is false, otherwise warnLog
Undocumented status codeFailLog

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.

Frequently asked questions

Spec validation checks the OpenAPI document against the OpenAPI rules. Response validation checks what the running API returns against the schemas in that document. You need both: the first proves the contract is well formed, the second proves the server keeps it.

Yes. Error bodies drift as often as success bodies, and clients parse them to show messages. Pick the schema for the returned 4xx or 5xx code, or the default response, and validate it the same way.

It can, because every new field becomes a contract change. Many teams leave it open in the public spec and validate against a stricter copy in their own tests, which still catches leaked fields without blocking normal additions.

Sources

  1. OpenAPI Specification 3.1.0: Responses ObjectOpenAPI Initiative
  2. Validating OpenAPI and JSON Schemajson-schema.org
  3. Ajv: JSON Schema drafts and the nullable keywordAjv documentation
  4. JSON Schema type reference (format as annotation)json-schema.org