Schema & Contract Testing
Validating responses against a JSON Schema in Postman so a changed API contract fails a test immediately.
Why validate the schema?
Checking a few fields (id, title) doesn't catch a renamed field, a number that became a string, or a field that suddenly disappears — exactly the changes that break mobile apps and front ends. A JSON Schema describes the full expected structure, and one assertion checks all of it.
This is contract testing in its simplest form: the API promises a shape, and your tests fail the moment it breaks that promise.
A JSON Schema in 60 seconds
{
"type": "object",
"required": ["id", "userId", "title", "body"],
"properties": {
"id": { "type": "integer" },
"userId": { "type": "integer" },
"title": { "type": "string", "minLength": 1 },
"body": { "type": "string" }
},
"additionalProperties": false
}
type— the expected JSON type.required— fields that must be present.properties— rules per field (types, lengths, ranges, formats, enums).additionalProperties: false— fail if the API returns unexpected fields (strict contract).
Validating in Postman
Postman supports JSON Schema validation directly with jsonSchema:
// GET {{baseUrl}}/posts/1 (baseUrl = https://jsonplaceholder.typicode.com)
const postSchema = {
type: 'object',
required: ['id', 'userId', 'title', 'body'],
properties: {
id: { type: 'integer' },
userId: { type: 'integer' },
title: { type: 'string', minLength: 1 },
body: { type: 'string' },
},
additionalProperties: false,
};
pm.test('Post matches the contract', () => {
pm.response.to.have.jsonSchema(postSchema);
});
When it fails, the test result says exactly which rule broke — e.g. data.userId should be integer.
Validating arrays
// GET {{baseUrl}}/comments?postId=1
const commentListSchema = {
type: 'array',
minItems: 1,
items: {
type: 'object',
required: ['postId', 'id', 'name', 'email', 'body'],
properties: {
postId: { type: 'integer' },
id: { type: 'integer' },
name: { type: 'string' },
email: { type: 'string', format: 'email' },
body: { type: 'string' },
},
},
};
pm.test('Comment list matches the contract', () => {
pm.response.to.have.jsonSchema(commentListSchema);
});
Reusing schemas across requests
Define a schema once and reuse it: store it as a collection variable in a collection-level pre-request script.
// Collection → Scripts → Pre-request
pm.collectionVariables.set('schema.post', JSON.stringify({
type: 'object',
required: ['id', 'userId', 'title', 'body'],
properties: {
id: { type: 'integer' },
userId: { type: 'integer' },
title: { type: 'string' },
body: { type: 'string' },
},
}));
// Any request returning a post
const schema = JSON.parse(pm.collectionVariables.get('schema.post'));
pm.test('Matches post schema', () => pm.response.to.have.jsonSchema(schema));
Using the OpenAPI spec as the source of truth
If your API has an OpenAPI specification, import it into Postman (Import → OpenAPI file). Postman generates a collection with every endpoint and example. The schemas inside the spec become your contract — when the spec changes, regenerate and the tests follow.
Schema testing vs consumer-driven contract testing
| Approach | What it checks | Tools |
|---|---|---|
| Schema validation | The response matches the published structure | Postman jsonSchema, Ajv, Playwright tests |
| Consumer-driven contracts | The API satisfies what each consumer actually uses | Pact |
Schema validation in Postman catches most breaking changes with very little effort. For microservice architectures with many consumers, look into Pact — see Testing Microservices.
What to check beyond the schema
A response can match the schema and still be wrong. Combine schema tests with value tests:
pm.test('Business rules', () => {
const post = pm.response.json();
pm.expect(post.id).to.eql(Number(pm.request.url.path.at(-1))); // the id we asked for
pm.expect(post.title.length).to.be.below(256);
});
Next: running the whole collection automatically with Data-Driven Runs & Newman.