Documentation menu

Validating API responses against a schema

Define the shape a response is supposed to have, then check real responses against it so contract drift fails loudly instead of silently.

Updated

Most API bugs that reach production are not outages. They are shape changes: a field renamed, a number that became a string, an array that is now null when empty. A status code will not catch any of those. A schema will.

Define what you expect

Open the Schema tab and describe the response in the schema language:

schema User {
  id:         string & format("uuid")
  email:      string & format("email")
  plan:       "free" | "pro"
  created_at: string & format("date-time")
  nickname?:  string          # optional — may be absent
  bio:        string?         # nullable — must be present, may be null
  teams:      Team[]
}

Fields are required by default. The two modifiers are the ones that matter most in practice, and they are independent:

  • nickname?: string — the key may be missing from the object.
  • bio: string? — the key must be there, but the value may be null.
  • avatar?: string? — both are allowed.

"Sometimes missing" and "sometimes null" are different failure modes, and a consumer that handles one often crashes on the other. Being explicit about which you mean is most of the value of writing the schema at all.

Constrain the values, not just the types

A type alone catches a lot, but the interesting bugs live in the values. Constraints chain with &:

type Username = string & minLength(3) & maxLength(32) & pattern("[a-z0-9_]+")
type Age      = integer & min(0) & max(150)
type Tags     = string[] & minItems(1) & maxItems(20) & uniqueItems

An integer & min(0) catches the negative quantity that a bare number would wave through. A union like "free" | "pro" catches the day someone adds a third plan and forgets to tell the client team.

Validate a real response

Send the request, then validate the response against the schema. You get a pass, or a list of exactly which fields diverged and how. That list is the useful part: it tells you whether the server changed or your expectation was wrong.

Validation is not limited to REST. The same schema file can describe GraphQL, WebSocket frames, SSE events and gRPC messages, so one contract covers every protocol your API speaks. Full syntax is in the schema language reference.

Three ways to get a schema

Write it by hand. Best when you own the API and know the contract.

Import an OpenAPI spec. If the API publishes OpenAPI 3.x, import it rather than retyping it — see importing an OpenAPI spec. The response schemas come across directly, which also surfaces a common surprise: the documented contract and the live response frequently disagree, and validating the real endpoint against its own spec is often the fastest way to prove it.

Generate from real traffic. Capture responses in the traffic monitor and derive a schema from what the endpoint actually returns. Generated schemas start permissive — the generator can only see the values it was given, so an ID that happened to be non-null in every sample looks non-nullable. Treat the generated file as a first draft and tighten it: narrow the unions, mark the genuinely optional fields, add the format constraints.

This third route is what makes validation possible for third-party APIs that never published a spec, which is the majority of them.

Run it in CI

A schema you check by hand catches drift the week you remember to look. To catch it the day it happens, run the check in a pipeline: @schemaclient/cli validates responses against the same schema files and exits non-zero on a mismatch, with a JUnit report for your CI to render. See running API tests in CI.

Run against a live or staging environment rather than a mock where you can. A mock returns what you told it to return, which encodes the very assumption being tested.

Where this pays off

  • Third-party APIs you do not control, which change without telling you.
  • Webhook payloads — see testing webhooks locally. Payload shapes evolve without warning and the failure is usually a quiet error in a background job.
  • Your own API, checked before release, so a serializer change does not ship a breaking rename to mobile clients that cannot be force-updated.

The broader case for this — and why a green test suite still misses it — is in when API tests pass but production breaks.

Schemas sync across your devices when you are signed in, alongside your collections.

Frequently asked questions

What is the difference between schema validation and a status code check?

A status code tells you the request was handled. A schema tells you the response is usable. An endpoint can return 200 OK with a null where your app expects an array — the status check passes and your app crashes anyway.

Can I generate a schema from an existing OpenAPI spec?

Yes. Import an OpenAPI 3.x document and the response schemas defined in it are turned into SchemaClient schemas, so you do not have to retype a contract you already wrote.

What if the API has no spec at all?

Capture real responses in the traffic monitor and generate a schema from what the endpoint actually returns, then tighten it by hand. This is the normal route for third-party APIs, which rarely publish a usable spec.

Is an optional field the same as a nullable one?

No, and conflating them is a common source of bugs. name?: string means the field may be absent. name: string? means it must be present but may be null. They are independent, and the validator reports them as different errors.