JSON Schema Validation

9 min read

Asserting on individual fields catches bugs you specifically wrote a test for. Schema validation catches the bugs you didn't think of — a renamed field, a missing required key, a string where a number used to be. A single schema check covers the entire response shape in one assertion, and it keeps doing so as the API evolves. This lesson teaches the format, how to write a schema for an existing response, and how schema validation slots into a real test suite.

What JSON Schema is

JSON Schema is a standard for describing JSON. It's itself written in JSON, and it answers questions like: "Which fields are required? What types are they? Are values constrained to a range or enum? Are extra fields allowed?"

A schema for a typical user response:

{
  "type": "object",
  "required": ["id", "name", "email", "createdAt"],
  "properties": {
    "id": { "type": "integer", "minimum": 1 },
    "name": { "type": "string", "minLength": 1, "maxLength": 100 },
    "email": { "type": "string", "format": "email" },
    "role": { "type": "string", "enum": ["admin", "editor", "viewer"] },
    "createdAt": { "type": "string", "format": "date-time" }
  },
  "additionalProperties": false
}

Read it top to bottom: this should be an object, with these four required keys, where id is a positive integer, name is a 1-100 character string, email is a string formatted like an email, role is one of three enum values, createdAt is an ISO 8601 timestamp, and no other keys are allowed.

A response and its schema, side by side

Response field → Schema rule

Response

  • {

    Object — the schema's top-level type.

  • "id": 42

    Integer ≥ 1.

  • "name": "Alice Smith"

    String, 1–100 chars.

  • "email": "alice@test.com"

    String, format: email.

  • "role": "admin"

    Enum: admin / editor / viewer.

  • "createdAt": "2026-05-01T10:00:00Z"

    String, format: date-time.

  • }

    additionalProperties: false — no extras allowed.

Schema rule

  • type: object

    Top-level value must be an object.

  • required + properties.id

    Field must be present, integer, ≥ 1.

  • properties.name (minLength 1, maxLength 100)

    String length bounded.

  • properties.email (format: email)

    Validator checks email shape.

  • properties.role (enum)

    Value must be one of the listed strings.

  • properties.createdAt (format: date-time)

    ISO 8601 timestamp.

  • additionalProperties: false

    Catches accidental new fields.

The pairing maps each field to one rule. When the API adds a field, the schema either accepts it (rule allows it) or rejects it (additionalProperties: false). Either choice is intentional — and your tests reflect it.

Why schema validation pays back

A schema check catches bug classes that field-by-field assertions miss:

  • Renamed fields. Backend renames email to emailAddress. Field tests pass because they happen not to check the new name. Schema fails because email is required.
  • Type changes. id flips from integer to string ("42"). Some clients silently coerce; the schema doesn't.
  • Missing required fields. createdAt accidentally omitted on one code path. Schema fails immediately.
  • Unexpected new fields. A debug field with internal data leaks into responses. additionalProperties: false catches it.
  • Type tightening. role adds a new value superadmin. Schema's enum constraint flags it for review.

Each one of these has shipped to production at real companies. A schema check on every test is cheap insurance.

Where schemas come from

Three common sources:

  • OpenAPI / Swagger specs. If your API has an OpenAPI document, you can extract a schema for each response directly. Tools like Schemathesis can auto-generate tests from the whole document.
  • Generated from sample responses. Tools like quicktype.io or genson convert example JSON to a starter schema. Hand-edit the result to add constraints (lengths, enums, regex).
  • Hand-written. For one or two endpoints, write the schema directly. It's a fast skill to pick up.

For a new endpoint, the practical workflow is: capture a known-good response, run it through genson, prune to what you actually want to enforce, and commit it next to the test.

Validating in code

Every language has a JSON Schema validator. The shape is the same:

# Python — pip install jsonschema
from jsonschema import validate
 
schema = {...}        # loaded from disk
data = response.json()
validate(instance=data, schema=schema)   # raises on mismatch
// JavaScript — npm install ajv ajv-formats
import Ajv from "ajv";
import addFormats from "ajv-formats";
 
const ajv = new Ajv();
addFormats(ajv);
const validator = ajv.compile(schema);
 
if (!validator(response.body)) {
  throw new Error(JSON.stringify(validator.errors));
}

In Postman, schema validation is a built-in step using pm.response.to.have.jsonBody() paired with tv4.validate() or Ajv-style checks.

A typical pattern is to have one helper that returns the parsed body only if the schema validates:

def parse_user(response):
    data = response.json()
    validate(data, USER_SCHEMA)
    return data

Tests then call user = parse_user(response) and assert on the data, knowing the shape is already verified.

Designing schemas for tests

A few patterns that work well in practice:

  • Be strict about required fields. Mark anything you want to guarantee is present. Optional fields are listed in properties but not in required.
  • Use additionalProperties: false deliberately. It's stricter — you'll need to update the schema when you add fields. That's a feature, not a bug; it forces a conscious decision when the contract changes.
  • Don't over-constrain at first. A schema that says "this field is a string" is useful. A schema that demands the string match a specific regex is brittle. Add tightness as bugs justify it.
  • Keep schemas next to tests, in version control. Your tests and your schemas evolve together; treat them as one unit.

What schema validation doesn't cover

A schema is a structural check. It can't tell you:

  • Whether total = sum(items[i].price) (business logic).
  • Whether createdAt is the correct timestamp for this operation.
  • Whether the user actually has the role the response claims.

Pair schema validation with a small number of business-logic assertions. Schema gives you broad coverage cheaply; targeted assertions cover the bits that matter.

⚠️ Common mistakes

  • Skipping additionalProperties: false because "we might add fields later." Defaulting to permissive means new accidental fields go unnoticed. Be deliberate — change it to true when you genuinely want extensibility.
  • Asserting on every field by hand instead of using a schema. A growing endpoint accumulates a sprawl of expect(body.field_42).toBe(...) checks that nobody maintains. One schema replaces all of them.
  • Treating schema generation as final. Auto-generated schemas allow null everywhere, miss enums, and don't include constraints. Always review and tighten.

🎯 Practice task

Write a schema and validate a response against it. 30 minutes.

  1. Pick a public endpoint with a known shape — https://jsonplaceholder.typicode.com/users/1 is a good starter.
  2. Run the request and copy the response.
  3. Paste the response into quicktype.io and choose "JSON Schema" as output. You'll get a starter schema.
  4. Tighten it: add additionalProperties: false, mark genuinely-required fields, add a format: email to the email field, add a length constraint to username.
  5. Install jsonschema (Python) or ajv (JS). Validate a fresh response against your schema. Confirm it passes.
  6. Modify the response in-memory: change id to a string, then validate. Confirm it fails with a clear error.
  7. Stretch: add a negative test that intentionally breaks one rule (e.g. an unexpected extra field) and confirms the validator catches it. Commit it next to the positive test.

Schema validation is a small habit that keeps paying off. The next lesson covers another always-on assertion: response time.

// tip to track lessons you complete and pick up where you left off across devices.