JSON Schema Validation in API Workflows
JSON Schema checks request and response shapes before your code runs. Use it in CI, at the gateway, and in docs so contract drift shows up early.
By Rehan Baqir
JSON Schema checks the shape of a request or response before business code runs. JSON Validator can tell you the text parses. It will not reject "quantity": "12" when the contract requires an integer. Schema covers types, required fields, enums, and formats. It does not know whether a coupon is expired or whether that user owns the cart. Put the check at the API boundary, and run the same schemas against fixtures in CI, or a string from a form field becomes a 500 inside billing.
Production 500s have traced back to quantity: "12", a string where a number belonged. Server TypeScript types were wrong. The client looked fine until a new mobile build sent strings from a form field. Nothing at the boundary rejected the body.
A schema at the door can return 400 Bad Request with a clear path: /quantity expected number.
Validation layers
| Layer | When | Catches | | --- | --- | --- | | CI fixtures | PR merge | Doc/sample drift | | Unit tests | Dev | Handler edge cases | | Runtime middleware | Request hit | Bad clients | | Response tests | PR | Breaking API changes | | Gateway (Kong, etc.) | Edge | External traffic |
Use more than one layer. A single check leaves a gap the others were meant to cover.
Authoring schemas
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"required": ["email", "quantity"],
"properties": {
"email": { "type": "string", "format": "email" },
"quantity": { "type": "integer", "minimum": 1 }
},
"additionalProperties": false
}
additionalProperties: false catches typos early. Public APIs sometimes leave it open for forward compatibility. Decide that internally.
Craft fixtures in JSON Formatter and check them with JSON Validator.
See JSON Formatting Guide and Common JSON Formatting Errors.
OpenAPI as source
Define components.schemas, generate TypeScript types with openapi-typescript, and generate validators with ajv.
Drift shows up when code changes and the spec does not. CI has to validate samples against the spec.
CI workflow example
- Keep
openapi.yamlin the repo - Keep example files such as
examples/create-order.json - Add a CI step:
ajv validate -s schema -d examples/*.json - In contract tests, assert the supertest response with
.toMatchSchema()
Pretty-print the failures. JSON Pretty Print CI covers redacting tokens.
Runtime (Node example)
import Ajv from "ajv";
const ajv = new Ajv({ allErrors: true });
const validate = ajv.compile(orderSchema);
if (!validate(req.body)) {
return res.status(400).json({ errors: validate.errors });
}
Return RFC 7807 problem+json so the error is explicit.
Versioning schemas
Freeze /v1/ schemas. Put additive changes on /v2/. Do not mutate a v1 schema in a breaking way. Add a new schema file.
UUID v4 vs v7 is the companion note for format: uuid in a schema.
JSON Schema vs JSONL
JSON vs JSONL: line-delimited logs need a schema check per line inside the stream processor.
Regex in schema
A pattern for codes should be tried in Regex Tester. Regex Email Truth shows where format checks go wrong.
Performance
Compile schemas once at startup. On a large payload, validate structure before you run deep business rules.
Partial validation for webhooks
Stripe-style webhooks can validate the event envelope first, then hand the nested object to a handler that has a schema for that event type. Fail fast on a malformed envelope. Keep the payload checks in the typed handler.
Schema versioning in CI
schemas/v1/order.json and schemas/v2/order.json can live together. CI should validate examples/v1/ against the matching version. Otherwise a v2 example can pass against the v1 schema by accident.
Error response quality
Return a JSON Pointer in the validation error: {"path": "/quantity", "message": "expected number"}. Mobile clients can highlight the field. A generic "validation failed" creates more support tickets.
Worked example: catching a string-vs-number bug at the API gateway
Suppose a checkout client sends "quantity": "2" (a string from an HTML <input>) where the API expects "quantity": 2 (an integer):
- Syntax check (JSON Validator): passes, because
"2"is valid JSON syntax. - JSON Schema check (
"type": "integer", "minimum": 1): fails immediately withinstancePath: "/quantity", message: "must be integer". The bad payload stops before downstream billing code concatenates"2" + 1into"21".
Limitations
JSON Schema validates structure, types, and formats. It does not validate database state or cross-service business rules. It can verify that coupon_code is a 6-to-12-character uppercase string and that user_id is a well-formed UUID. Application logic still has to check whether that coupon is expired and whether that user owns the cart.
Conclusion
Keep a schema at the boundary and fixtures in CI. TypeScript types on the server do not survive the hop over HTTP.
Return 400 with a path. A quantity sent as a string should die at the door, before accounting sees it.
Frequently Asked Questions
Common questions answered to help you get the most from this tool.