OpenAPI, Contract & Schema Testing

API Contract Testing: What It Is, How It Works and Tools

Sushant JoshiUpdated Oct 19, 202621 min read

Quick answer

API contract testing checks that an API and its consumers still agree on a shared, machine-readable contract: an OpenAPI spec or a Pact file covering endpoints, fields, types and status codes. Tests run in CI on every change, so a renamed field or removed response fails the pull request instead of breaking a client in production.

Reviewed by Rishi Gaurav

Share:
API contract testing diagram showing provider and consumer validating against a shared OpenAPI contract

API contract testing verifies, on every change, that an API provider and the clients that call it still agree on a shared, machine-readable interface: which endpoints exist, what the request and response bodies look like, which fields are required, which status codes and headers are valid, and how errors are represented. The contract is usually an OpenAPI document (schema-first) or a set of Pact files that consumers publish (consumer-driven). The tests run inside the pull request, so a breaking change fails a build instead of a customer's app.

Every distributed system eventually hits the same incident. A backend engineer renames user_name to username in a response. The OpenAPI file is not updated. Unit tests pass, the integration build is green, the change ships, and forty minutes later a mobile client starts throwing parse errors. Nothing was watching the boundary between the two services. Contract testing is the thing that watches it.

This guide is the hub for our OpenAPI, contract and schema content. It covers what a contract is, a beginner-friendly example, how contract testing differs from unit, integration and API testing, the three approaches (schema-first, consumer-driven and snapshot), a step-by-step CI workflow with a breaking-change table, and how to pick tools. The deeper dives live in the spokes linked from each section, starting with contract testing tools compared and API schema validation.

In this guide

  1. What is API contract testing?
  2. Contract testing vs unit, integration and API testing
  3. Three approaches: schema-first, consumer-driven and snapshot
  4. How to do API contract testing, step by step
  5. API contract testing tools
  6. Common mistakes and best practices
  7. API contract testing FAQ

What is API contract testing?

An API contract is the published agreement between a producer and its consumers. It states what endpoints exist, what inputs they take, what outputs they return and under what conditions. A contract only counts for testing when it is explicit and machine-readable. In practice it lives in one of these artifacts:

InterfaceContract artifact
REST / HTTPOpenAPI 3.x (or Swagger 2.0) document
GraphQLSDL schema
SOAPWSDL plus XSD
gRPC.proto files
Events (Kafka, AMQP, MQTT)AsyncAPI document, or schemas in a schema registry
Consumer-drivenPact files, one per consumer-provider pair

API contract testing is the automated practice of proving, on every change, that both sides still honour that artifact. The provider must return responses that match it. Consumers must send requests that conform to it. When either side diverges, a test fails in CI and the merge is blocked.

Contract testing is deliberately narrow. It does not check that creating an order deducts inventory, that an endpoint is fast, or that authorization is correct. It answers one question: does the interface still match what was promised? That narrowness is why it is cheap. Contract checks touch only the interface layer, so they are fast, deterministic and easy to run in parallel, and because they run on every commit they catch interface regressions before the code is merged.

A beginner-friendly API contract testing example

Take one endpoint, GET /orders/{id}, and one consumer, a mobile app that shows an order's status and total. Here is the relevant part of the OpenAPI contract:

paths:
  /orders/{id}:
    get:
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Order found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Order' }
        '404':
          description: No order with this id
components:
  schemas:
    Order:
      type: object
      required: [id, status, total]
      properties:
        id:     { type: string }
        status: { type: string, enum: [pending, paid, shipped, cancelled] }
        total:  { type: number, minimum: 0 }

A contract test does three things: call the endpoint, check the status code is one the contract documents, and validate the body against the Order schema.

// contract.test.js — schema-first check against the OpenAPI contract
import Ajv from 'ajv';
import yaml from 'js-yaml';
import { readFileSync } from 'node:fs';

const spec = yaml.load(readFileSync('openapi.yaml', 'utf8'));
const ajv = new Ajv({ allErrors: true, strict: false });
const validateOrder = ajv.compile(spec.components.schemas.Order);

test('GET /orders/{id} honours the Order contract', async () => {
  const res = await fetch(`${process.env.API_URL}/orders/123`);
  expect([200, 404]).toContain(res.status);          // documented codes only
  if (res.status === 200) {
    const body = await res.json();
    if (!validateOrder(body)) throw new Error(ajv.errorsText(validateOrder.errors));
  }
});

Now a developer renames total to amount. The handler still works and its unit tests still pass. The contract test fails with must have required property 'total', and the pull request goes red before the mobile app ever sees the change. If the developer adds a new status refunded without updating the enum, the same test fails on the enum rule. That is the whole idea: the contract is the source of truth, and the implementation is not allowed to drift from it silently.

For how the schema keywords in that example behave (required, enum, $ref, formats and drafts), see the dedicated JSON Schema validation guide; for the request and response validation patterns built on them, see API schema validation.

Contract testing vs unit, integration and API testing

These terms get used interchangeably, but they answer different questions and catch different bugs.

Unit testingContract testingIntegration testingAPI testing (general)
Question answeredDoes this function work?Does the interface still match the promise?Do these components work together?Does this API behave correctly?
ScopeOne function or class, collaborators mockedThe wire format between two servicesReal interactions between real componentsAny endpoint: behaviour, rules, errors, security, speed
DependenciesNoneNone (mock or recorded contract)Real or containerized servicesVaries
SpeedMillisecondsMilliseconds to secondsSeconds to minutesVaries
Typical catchLogic bugsRenamed fields, type changes, removed responsesWiring bugs, wrong queries, broken auth flowFunctional bugs, validation, business logic

Contract testing vs unit testing. A unit test lives inside one service and knows nothing about the network interface. A service can have every unit test passing and still break all its consumers by renaming a JSON field, because the serializer is outside what the unit test asserts. Contract tests sit exactly on that seam.

Contract testing vs API testing. "API testing" is the umbrella. Contract testing is the slice of it that checks structure: fields, types, required properties, status codes, headers. A functional API test asks "did POST /orders create an order with the right total?"; a contract test asks "did it return a 201 with a body shaped like Order?". You need both, and contract tests are the cheaper half to automate because the contract already states what correct looks like.

Contract testing vs integration testing. Integration tests prove that services actually work together, but they need a shared environment and get slower and flakier as the service count grows. Contract tests verify each side on its own against the agreement, so each service can deploy independently. See unit vs integration vs system testing for where each sits in the test pyramid.

Three approaches: schema-first, consumer-driven and snapshot

Schema-first (provider-side)

The provider owns a specification, usually OpenAPI, and the running implementation is validated against it. A test runner sends real requests and checks each response against the schema for that operation and status code.

┌──────────┐   real requests   ┌────────┐
│   Test   │ ────────────────> │ Server │
│  runner  │ <──── JSON ────── │        │
└──────────┘                   └────────┘
      │ validate against openapi.yaml
      ▼
    pass / fail

Schema-first is the easiest place to start: if you already maintain an OpenAPI file, you already have the contract. It also covers consumers you do not know about, such as public API users. Its blind spot is that it proves the provider matches its spec, not that the spec covers what each consumer actually depends on. Tools: Schemathesis, Dredd, Prism in proxy mode, Specmatic, and spec-driven platforms.

Score your OpenAPI spec (free)

Free, runs in your browser, no sign-up needed.

Consumer-driven (Pact-style)

Each consumer publishes the expectations it has of the provider, and the provider must satisfy all of them before it deploys. The consumer only describes the fields it reads, so the contract is minimal.

// Consumer side (Pact): record what this consumer needs
await provider
  .given('an order with ID 123 exists')
  .uponReceiving('a request for order 123')
  .withRequest({ method: 'GET', path: '/orders/123' })
  .willRespondWith({
    status: 200,
    body: { id: '123', status: like('shipped'), total: like(49.99) },
  });

Running the consumer tests writes a Pact file, which is published to a Pact Broker. The provider's CI fetches every pact for its version and replays each interaction against the real service; can-i-deploy then answers whether a given version is safe to release to an environment. Consumer-driven contracts give sharper protection for a known set of internal consumers, at the cost of coordination: every consumer team needs the Pact library, provider states and a broker. Tools: Pact with the open-source Pact Broker or PactFlow, and Spring Cloud Contract on the JVM.

Bi-directional contract testing sits between the two: the provider publishes its OpenAPI document, consumers publish their pacts, and the broker checks one against the other without replaying interactions against a running provider. PactFlow offers this. Our Pact vs Postman for contract testing post covers what a schema assertion can and cannot prove compared with a real consumer contract.

Snapshot-based

Record live responses once, commit them, and compare every later run against the snapshot. Any difference fails.

real server → capture JSON → commit to repo → compare on the next run

Snapshots are quick to set up and useful for large, loosely specified responses. They are brittle: any change, including a harmless new field, fails the test, and teams quickly learn to run --update whenever a snapshot breaks, at which point the snapshot documents the latest change rather than the intent. Use them sparingly, next to a real contract rather than instead of one.

Most mature teams combine the approaches: schema tests on every build, Pact verification for the few inter-service relationships where a break is expensive, and snapshots only for responses the schema cannot describe well.

How to do API contract testing, step by step

1. Make the contract explicit and version it

The contract lives in the service repository, next to the code, and changes in the same pull request as the code. If the spec is written after the code, it describes the bug rather than the intent. Choose which side is authoritative: generating the spec from code on every build means every code change silently becomes a contract change, so if you generate, review the generated diff like any other contract change. For the spec-first loop end to end, see schema-first API development and testing; which OpenAPI version you are on matters too, because OpenAPI 3.1 and 3.0 treat nullable, exclusiveMinimum and examples differently.

2. Lint the spec

Run a linter such as Spectral as a required check. A useful ruleset requires documented error responses (400, 401, 403, 404, 409, 422, 500 where they apply), example values, descriptions, and required arrays on response schemas. Contract tests against a spec that only documents the 200 path are green and meaningless.

3. Decide what the schema tests cover

For every endpoint in the spec, the schema-first suite should check that:

  1. every documented status code is reachable with an appropriate request;
  2. the response body matches the schema for that status code;
  3. required fields are always present;
  4. nullable fields are either null or the declared type;
  5. enum fields only contain declared values;
  6. nested objects match their referenced schemas;
  7. the response Content-Type matches the spec;
  8. documented headers are present, such as Location on a 201.

Tools can derive all of that from an OpenAPI file. What they cannot decide for you is which business-rule combinations matter, which specific status code a given test should trigger, or how to obtain realistic credentials. Contract tests plus hand-written business tests is what full coverage looks like.

4. Classify changes as breaking or non-breaking

Every contract change falls into one of three buckets. This table is the policy most teams converge on:

ChangeBreaking?What to do
Add a new endpointNoSafe to ship
Add an optional field to a responseNo (unless consumers validate strictly)Safe for tolerant readers
Make a required request field optionalNoSafe
Remove a field from a responseYesDeprecate first, remove in the next major version
Rename a fieldYesAdd the new field, deprecate the old, remove later
Make an optional request field requiredYesExisting clients will start getting 400s
Change a field's typeYesMajor version bump
Remove an enum valueYesBreaking
Add an enum valueMaybeBreaking for consumers that switch exhaustively
Remove a documented success responseYesBreaking
Change a default valueYesSilent behaviour change

Diff tools such as oasdiff and openapi-diff apply rules like these automatically to two versions of a spec; graphql-inspector does the same for GraphQL schemas, and the Pact Broker's can-i-deploy does it at the level of verified consumer contracts.

5. Wire it into the pull request

For an API with an OpenAPI contract, the CI shape looks like this:

  1. A pull request opens.
  2. CI runs the schema-test suite against the branch's build.
  3. CI diffs the branch's OpenAPI file against main's.
  4. The diff classifies each change as safe or breaking.
  5. Breaking changes require an explicit, reviewed override (for example a breaking-change-approved label and a version bump).
  6. On merge, the spec is published to wherever consumers read it (a registry, a portal, or the repo).
  7. Consumer teams are notified of the new version.
git show origin/main:openapi.yaml > /tmp/base.yaml
oasdiff breaking /tmp/base.yaml openapi.yaml --fail-on ERR

This turns contract drift from a gradual decay into a discrete, reviewable event. For pipeline recipes see API test automation with CI/CD.

API contract testing with Postman

Postman can do schema-first checks inside a collection. A test script validates the response body against a JSON Schema:

// Postman test tab
const orderSchema = {
  type: 'object',
  required: ['id', 'status', 'total'],
  properties: {
    id: { type: 'string' },
    status: { type: 'string', enum: ['pending', 'paid', 'shipped', 'cancelled'] },
    total: { type: 'number', minimum: 0 },
  },
};
pm.test('response honours the Order contract', () => {
  pm.response.to.have.status(200);
  pm.response.to.have.jsonSchema(orderSchema);
});

Two limits are worth knowing. The schema is usually copied into the test, so it can drift from the OpenAPI file unless you load it from the spec. And Postman checks the provider against a schema; it does not capture consumer expectations and verify them against the provider, which is what makes a contract consumer-driven. Teams that need that add Pact. The Pact vs Postman post goes deeper.

Contract tests for event-driven APIs

REST and GraphQL are not the only contracts. Event-driven systems (Kafka, RabbitMQ, SNS) need contract testing too:

  • A schema registry (Confluent Schema Registry, Apicurio) stores the schema of each event type.
  • Producers register a schema version when publishing; consumers read with the version they understand.
  • Compatibility modes (backward, forward, full) decide which schema changes the registry accepts.
  • AsyncAPI documents the channels and messages; Pact's message pacts cover consumer-driven checks for events.

API contract testing tools

The tool landscape splits into three groups, and most teams use one from each:

  • Consumer-driven: Pact (with the open-source Pact Broker or PactFlow) and Spring Cloud Contract.
  • Schema-first and spec-driven: Specmatic, Schemathesis, Dredd, Prism, Microcks, and spec-driven test platforms.
  • Spec quality and diffing: Spectral for linting; oasdiff, openapi-diff and Optic for breaking-change detection.

Free PDF + code examples

OpenAPI to Test Generation Template Pack

Go from OpenAPI spec to full test coverage. Includes sample specs, example generated tests, edge case patterns, and CI/CD integration guides.

Download Free

The full side-by-side comparison, with selection criteria, Java options and open-source picks, is in contract testing tools compared, so it is not repeated here.

Where generation fits. Hand-writing schema checks for a few hundred endpoints is the part teams never finish. Spec-driven generation reads the OpenAPI document and produces positive, negative and boundary tests per operation, so the schema-first baseline exists from day one; how AI generates API tests from OpenAPI explains the mechanics. Shift-Left API is one example of this approach: it imports an OpenAPI, Swagger, WSDL or GraphQL contract, plans tests deterministically from the spec without needing an AI key, and adds field and type checks derived from each response schema. When a spec is re-imported it diffs the new version against the current endpoints, classifies each change as breaking or non-breaking, and holds it for an Approve & Import or Reject decision; approving creates new endpoint versions and does not edit existing tests. Passing runs are also checked for responses that no longer match the documented contract (a missing required field, a changed primitive type, an undeclared enum value). It is a schema-first tool, not a Pact broker. See API contract testing with Shift-Left API.

Measure what you cover. Track the share of endpoints, methods and documented status codes that have contract checks, and do not let new endpoints ship without them. How to measure API test coverage defines the dimensions, and the free OpenAPI coverage tool scores a spec in your browser.

Common mistakes and best practices

Mistakes that make contract testing fail

  1. "We'll write the OpenAPI later." Contract tests need a contract. A spec written after the code documents whatever the code happens to do.
  2. Trusting the happy path. A suite that only covers 200 responses misses half the contract. Every documented error response needs a test, and an undocumented one is a spec bug.
  3. No diff-based gating in CI. If a breaking change can reach main without a reviewed override, the rest of the apparatus does not matter.
  4. Pact without a broker. Pact's value comes from centralizing consumer expectations and can-i-deploy. Without a broker you are running extra tests that nobody consults before a release.
  5. Snapshots that update on every PR. If --update is the reflex response to a red snapshot, the snapshot is no longer an assertion.
  6. Contracts that mirror the whole response. A consumer contract should list only the fields that consumer reads. A pact that copies the provider's full schema fails every time the provider adds a field, and teams start ignoring it.
  7. Flaky data. Contract tests that depend on specific database rows or shared environments fail for reasons unrelated to the interface. Use provider states, ephemeral data and mocks, and assert shape rather than specific values.

Best practices

  • Treat the contract as code. Review contract changes in pull requests, version them with the implementation, and require an API owner's approval on spec changes.
  • Use schema-first as the baseline and consumer-driven where it pays. Start with the spec you have. Add Pact for the relationships where a break is expensive: payments, authentication, partner-facing APIs.
  • Gate merges, not just deployments. A contract failure found at deploy time has already cost a review cycle.
  • Automate what the contract implies; hand-write what it cannot. Generate schema validation, per-field negative tests, status-code reachability and the breaking-change diff. Write business scenarios that cross endpoints, multi-step flows and domain edge cases by hand.
  • Version and deprecate explicitly. Use deprecated: true in OpenAPI, give consumers a migration window, and run old and new contract versions side by side during the transition.
  • Mark only what you guarantee as required. Every required field is a promise to every consumer. Think before adding additionalProperties: false to response schemas: it catches accidental data leaks, but turns every new field into a breaking change for strict validators.
  • Keep PR feedback fast. Shard contract tests in parallel and keep the pull-request run in single-digit minutes, or developers will route around it.
  • Remember what contract tests do not cover. They catch interface drift. Business-logic bugs, slow endpoints and authorization flaws need functional, performance and security tests alongside them.

A minimal adoption path: commit and lint a spec for one pilot service, generate or write its schema-first suite, add the breaking-change diff to its pull requests, gate merges on both, then expand service by service. Add a Pact Broker when a second consumer team's expectations start diverging from the published spec.

More from the OpenAPI, Contract & Schema Testing hub

Every guide in this hub, grouped by format. The OpenAPI, Contract & Schema Testing hub page has the full list.

Guides and concepts

How-to tutorials

Tools and comparisons

API contract testing FAQ

What is an example of API contract testing?

A mobile app reads the fields id, status and total from GET /orders/{id}. The OpenAPI contract marks those three fields as required and gives their types. A contract test calls the endpoint and validates the response against that schema. If a developer renames total to amount, the response no longer matches, the test fails in the pull request, and the app never sees the broken payload.

What is the difference between contract testing and unit testing?

A unit test checks one function or class inside a service, with its collaborators mocked, and knows nothing about the network interface. A contract test checks the boundary between two services: that the requests and responses on the wire still match the agreed contract. A service can have 100% passing unit tests and still break every consumer by renaming a JSON field; only a contract test notices.

What is the difference between contract testing and API testing?

API testing is the broad category: functional behaviour, business rules, error handling, security and performance of an API. Contract testing is a narrow slice of it that only asks whether the interface still matches the published agreement: fields, types, required properties, status codes and headers. It does not check that an order was actually created correctly, which is why teams run both.

How do you do API contract testing?

Pick the contract artifact (an OpenAPI spec for schema-first, or Pact files for consumer-driven), store it in version control, and lint it. Generate or write tests that validate every documented response against the schema, run them in CI on every pull request, diff the spec against main to classify breaking changes, and block merges that break the contract without an explicit, reviewed version change.

Can you do API contract testing with Postman?

Partly. A Postman test can validate a response body against a JSON Schema with pm.response.to.have.jsonSchema(), which gives you provider-side schema checks per request. Postman does not record consumer expectations or verify them against a provider the way Pact does, so it covers schema-first checks but not consumer-driven contracts. Teams often pair Postman or a spec-driven tool with Pact.

Do I still need contract testing if I have integration and end-to-end tests?

Yes. Integration and end-to-end tests run late, need shared environments and get slower as the number of services grows. Contract tests verify each service's interface on its own, in seconds, inside the pull request. In most pipelines contract tests take over the interface checks, and end-to-end tests stay for a few critical user journeys.

Sources and further reading

Key takeaways

  • API contract testing checks one thing, whether the interface still matches the published agreement, and checks it on every pull request.
  • Schema-first (OpenAPI) is the cheapest baseline and covers unknown consumers; consumer-driven (Pact) adds sharper protection for the relationships where a break is expensive.
  • A useful suite validates every documented response, including error responses, and pairs it with a breaking-change diff that blocks unreviewed contract changes.
  • Contract tests complement unit, functional, performance and security tests; they do not replace them.

Want to see a schema-first contract suite generated from your own OpenAPI spec, with breaking-change review on every re-import? Start a free trial and import a spec.

Continue learning

Go deeper in the Learning Center

Hands-on lessons with runnable code against our live sandbox.

Ready to shift left with your API testing?

Import your OpenAPI spec and get a runnable test suite, in our cloud or on your own infrastructure.