JSON Schema · API testing · Scripts · Tutorial

JSON Schema validation in API tests

How to write a JSON Schema for an API response, validate it with swc.response.to.have.jsonSchema, pick the right draft, fix the usual mistakes and apply it across a collection.

By · · 8 min read

To validate an API response against a JSON Schema in a test, define the schema as a JavaScript object in the post-response script and assert swc.response.to.have.jsonSchema(schema) inside swc.test. Swastik Codes validates with Ajv 6, which implements JSON Schema draft-07, and the failing test lists every mismatch, such as data.items[0].price should be number. The same line works as pm.response.to.have.jsonSchema(schema) in scripts written for Postman.

This guide explains what JSON Schema is, which draft to write, how to build a schema for a real response, and the mistakes that make schema tests pass when they should fail.

What JSON Schema is

JSON Schema is a vocabulary for describing the shape of JSON data: which type each value has, which properties are required, which values are allowed. A schema is itself a JSON document:

JSON
{
  "type": "object",
  "required": ["id", "email"],
  "properties": {
    "id": { "type": "integer", "minimum": 1 },
    "email": { "type": "string", "format": "email" }
  }
}

In API testing a schema replaces dozens of field-by-field checks. Instead of asserting that id is a number, email is a string and address is present, you describe the response once and validate the whole body in one test. It catches the problems individual assertions miss: a field that turned into a string, a property that disappeared, a null where the client expects an object.

Which draft to use

JSON Schema has gone through several drafts. The main ones in use:

DraftNotable changesIn the script sandbox
draft-04 (2013)id, boolean exclusiveMinimumvia the bundled tv4 library
draft-06 (2017)const, contains, numeric exclusiveMinimumkeywords work under draft-07
draft-07 (2018)if/then/else, readOnly, writeOnlysupported by jsonSchema (Ajv 6)
2019-09unevaluatedProperties, $defs, dependentRequirednot supported
2020-12prefixItems, $dynamicRefnot supported

Write draft-07 schemas and leave out $schema, or set it to http://json-schema.org/draft-07/schema#. A $schema pointing at draft-04, draft-06, 2019-09 or 2020-12 makes the test fail with no schema with key or ref, because only the draft-07 meta-schema is loaded.

OpenAPI uses JSON Schema too, with differences. OpenAPI 3.0 schemas are a modified subset of an older draft and add nullable; OpenAPI 3.1 aligns with 2020-12. Copying a schema from a spec usually works, with the caveats in the mistakes section below. If the API has an OpenAPI file, you can also have the tests generated; see importing an OpenAPI spec and generating tests.

Write a schema for a real response

Take an order endpoint that returns:

JSON
{
  "id": "ord_8f2k1",
  "status": "paid",
  "createdAt": "2026-10-23T09:12:44Z",
  "customer": { "id": 42, "email": "ada@example.com" },
  "items": [
    { "sku": "BK-101", "quantity": 2, "price": 12.5 }
  ],
  "total": 25.0,
  "couponCode": null
}

Write the schema from the contract you expect, not just from one sample. Mark what is always present as required, constrain enums and patterns, and say explicitly which fields may be null:

JavaScript
const orderSchema = {
  type: 'object',
  required: ['id', 'status', 'createdAt', 'customer', 'items', 'total'],
  additionalProperties: false,
  properties: {
    id: { type: 'string', pattern: '^ord_[a-z0-9]+$' },
    status: { type: 'string', enum: ['pending', 'paid', 'shipped', 'cancelled'] },
    createdAt: { type: 'string', format: 'date-time' },
    customer: {
      type: 'object',
      required: ['id', 'email'],
      properties: {
        id: { type: 'integer', minimum: 1 },
        email: { type: 'string', format: 'email' }
      }
    },
    items: {
      type: 'array',
      minItems: 1,
      items: { $ref: '#/definitions/lineItem' }
    },
    total: { type: 'number', minimum: 0 },
    couponCode: { type: ['string', 'null'] }
  },
  definitions: {
    lineItem: {
      type: 'object',
      required: ['sku', 'quantity', 'price'],
      properties: {
        sku: { type: 'string' },
        quantity: { type: 'integer', minimum: 1 },
        price: { type: 'number', minimum: 0 }
      }
    }
  }
};

definitions with a local $ref lets you describe a repeated shape once. (2019-09 renamed this to $defs; under draft-07, definitions is the conventional name.)

Validate it in a test

Paste the schema into the request's post-response script, in the Scripts tab, and add the test:

JavaScript
swc.test('status is 200', function () {
  swc.response.to.have.status(200);
});

swc.test('order matches the schema', function () {
  swc.response.to.have.jsonSchema(orderSchema);
});

The editor also offers a Validate JSON schema snippet as a starting point. Results appear in the Test Results tab. When the body does not match, the message lists all errors, because Ajv runs with allErrors:

Text
expected data to satisfy schema but found following errors:
data.status should be equal to one of the allowed values, data.items[0].quantity should be integer

The paths tell you exactly where to look. If the body is not JSON at all, it is validated as undefined and the test fails, so keep a separate status test: the first failure then explains the second.

To validate part of a response, or any value, use swc.expect:

JavaScript
const body = swc.response.json();

swc.test('first item matches the line item schema', function () {
  swc.expect(body.items[0]).to.have.jsonSchema(orderSchema.definitions.lineItem);
});

jsonSchema takes Ajv options as a second argument, which is useful for the format and nullable cases below:

JavaScript
swc.response.to.have.jsonSchema(schema, { unknownFormats: 'ignore', nullable: true });

If you prefer to call a validator directly, require('ajv') and require('tv4') are both bundled. tv4 targets draft-04 and is mostly found in older collections:

JavaScript
swc.test('valid with tv4', function () {
  swc.expect(tv4.validate(swc.response.json(), schema), tv4.error && tv4.error.message).to.be.true;
});

Common mistakes

Schema tests fail quietly in one direction: a loose schema passes almost anything. These are the problems that come up most.

Forgetting required

properties only describes a property if it is present. Without required, an empty object {} passes the schema above. List every field the client depends on in required, at every level of nesting.

Leaving additionalProperties open, or closing it in the wrong place

By default extra properties are allowed, so a misspelled field (totl instead of total) passes. additionalProperties: false rejects unknown fields. Decide deliberately: closed schemas catch typos and accidental data leaks, open ones tolerate an API adding fields. A common compromise is closed at the top level and open in nested objects you do not own.

additionalProperties: false only knows the properties declared next to it. If you compose a schema with allOf, properties defined inside the allOf branches count as additional and fail. Draft 2019-09 added unevaluatedProperties for this, but it is not available in draft-07, so repeat the property names at the level that sets additionalProperties: false.

Expecting nullable to work

nullable: true is an OpenAPI 3.0 keyword, not JSON Schema. Ajv ignores unknown keywords by default, so { "type": "string", "nullable": true } still rejects null. Use a type array, { "type": ["string", "null"] }, or pass { nullable: true } as the Ajv option shown above when you paste schemas from an OpenAPI 3.0 file.

Formats that are too strict, or unknown

Ajv checks common formats such as date-time, date, email, uri, uuid, ipv4 and ipv6. date-time follows RFC 3339 and requires a time zone, so 2026-10-23T09:12:44 fails while 2026-10-23T09:12:44Z passes. That is usually a real finding about the API, not a schema bug.

Formats Ajv does not know, such as OpenAPI's int32, int64, float or binary, make validation throw unknown format "int64" is used in schema. Remove them, or pass { unknownFormats: 'ignore' }.

integer versus number

integer rejects 12.5, and number accepts both. Money is often serialized as a decimal, so check what the API really sends. Also note that "1" is a string: a schema with type: 'integer' correctly fails an API that quotes its numbers.

Typos in keywords

A misspelled keyword such as require or maxlength is ignored without an error, so the constraint silently does nothing. An invalid value for a known keyword, such as type: 'strin', makes the schema invalid and the test fails with schema is invalid. When a schema test passes suspiciously easily, send a response you know is wrong and confirm it fails.

Validating the wrong root

If the endpoint returns an array, the root schema must be type: 'array' with items. A list response wrapped in an envelope ({ "data": [...], "next": "..." }) needs an object schema with data inside it.

Apply schemas across a collection

Copying schemas into every request is hard to maintain. Two approaches scale better.

One collection-level script. Post-response scripts on a collection run after every request in it. Keep the schemas there, keyed by request name, and validate whichever one applies:

JavaScript
// Collection > Scripts > Post-response; define orderSchema above this line
const schemas = {
  'Get order': orderSchema,
  'List orders': { type: 'array', items: orderSchema }
};

const schema = schemas[swc.info.requestName];
if (schema) {
  swc.test('response matches the schema for ' + swc.info.requestName, function () {
    swc.response.to.have.jsonSchema(schema);
  });
}

Generated from a spec. If the API is described in OpenAPI, the spec view in APIs generates contract tests that validate each response against its documented schema, converted to draft-07 (nullable becomes a null type, formats are dropped). Importing an OpenAPI spec and generating tests walks through it.

Then run the collection in the collection runner. Every request's schema test shows up in the run summary, data files can drive the same requests with different inputs (data-driven API testing), and the results export as JUnit XML for CI. A monitor can run the same collection on a schedule and alert you when a response stops matching its schema.

Requests from the web app go through the swastik.codes server, which needs a free account and does not reach private or loopback addresses; for an API on your own machine, use the desktop app for Windows or Linux.

Frequently asked questions

Which JSON Schema validator do the test scripts use?

jsonSchema assertions use Ajv 6 with allErrors enabled, which implements draft-07. tv4 (draft-04) is also bundled for older scripts, and require('ajv') gives you Ajv directly.

Does it support JSON Schema 2020-12?

No. Schemas that declare $schema as 2019-09 or 2020-12 fail, and keywords introduced in those drafts, such as unevaluatedProperties and prefixItems, are not applied. Most response schemas can be written in draft-07 without loss.

How do I allow null for a field?

Use a type array: { "type": ["string", "null"] }. The OpenAPI nullable keyword only works if you pass { nullable: true } as the second argument to jsonSchema.

Can I validate a nested object or an array item only?

Yes. Use swc.expect(value).to.have.jsonSchema(schema) with any value from swc.response.json().

Is schema validation enough on its own?

No. A schema checks shape and types, not meaning. Combine it with value checks: the right id came back, totals add up, the status code is the documented one. Writing API tests with swc.* scripts covers those assertions.

Try it on your own API

Swastik Codes runs in the browser and is free to use. Create an account to send requests, sync your workspaces and invite your team.