JSON Schema Validation: A Practical Guide with Examples
Complete guide to JSON Schema validation — write schemas for real API shapes, validate JSON in JavaScript and Python, and catch errors before they reach production.
Have broken JSON right now? Fix it free in under 1 second — no signup.
Fix My JSON →JSON Schema is a vocabulary that allows you to annotate and validate JSON documents. It's the standard way to define the shape of your data — the types of values, required fields, and allowed formats. This guide explains how JSON Schema works with practical examples you can use immediately.
What Is JSON Schema?
A JSON Schema is itself a JSON document that describes the structure another JSON document must follow. Think of it as a contract for your data.
{
"type": "object",
"properties": {
"name": { "type": "string" },
"age": { "type": "integer", "minimum": 0 }
},
"required": ["name"]
}
This schema says: "I expect a JSON object with a name property (required, must be a string) and an optional age property (must be a non-negative integer)."
Any JSON document that matches this schema is called valid. One that doesn't is invalid.
Core JSON Schema Keywords
type
Restricts the value to a specific JSON type:
{ "type": "string" } // strings only
{ "type": "number" } // integers and decimals
{ "type": "integer" } // integers only
{ "type": "boolean" } // true or false
{ "type": "array" } // arrays only
{ "type": "object" } // objects only
{ "type": "null" } // null only
// Multiple types allowed:
{ "type": ["string", "null"] } // string or null
properties and required
{
"type": "object",
"properties": {
"id": { "type": "integer" },
"name": { "type": "string", "minLength": 1 },
"email": { "type": "string", "format": "email" },
"role": { "type": "string", "enum": ["admin", "user", "viewer"] }
},
"required": ["id", "name", "email"],
"additionalProperties": false
}
additionalProperties: false means the object cannot have any keys not listed in properties. This is useful for strict API contracts.
items (for arrays)
{
"type": "array",
"items": { "type": "string" },
"minItems": 1,
"maxItems": 50,
"uniqueItems": true
}
This schema validates: a non-empty array of up to 50 unique strings.
$ref and $defs (reusable schemas)
{
"$defs": {
"Address": {
"type": "object",
"properties": {
"street": { "type": "string" },
"city": { "type": "string" },
"zip": { "type": "string", "pattern": "^\\d{5}$" }
},
"required": ["street", "city", "zip"]
}
},
"type": "object",
"properties": {
"billing": { "$ref": "#/$defs/Address" },
"shipping": { "$ref": "#/$defs/Address" }
}
}
Define a schema once and reference it in multiple places with $ref.
Real-World Schema Examples
User Profile API Response
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"id": { "type": "string", "format": "uuid" },
"username": { "type": "string", "minLength": 3, "maxLength": 30, "pattern": "^[a-zA-Z0-9_]+$" },
"email": { "type": "string", "format": "email" },
"createdAt": { "type": "string", "format": "date-time" },
"plan": { "type": "string", "enum": ["free", "pro", "enterprise"] },
"tags": { "type": "array", "items": { "type": "string" }, "default": [] }
},
"required": ["id", "username", "email", "createdAt", "plan"]
}
Paginated List Response
{
"type": "object",
"properties": {
"data": {
"type": "array",
"items": { "$ref": "#/$defs/Item" }
},
"pagination": {
"type": "object",
"properties": {
"page": { "type": "integer", "minimum": 1 },
"perPage": { "type": "integer", "minimum": 1, "maximum": 100 },
"total": { "type": "integer", "minimum": 0 },
"totalPages": { "type": "integer", "minimum": 0 }
},
"required": ["page", "perPage", "total", "totalPages"]
}
},
"required": ["data", "pagination"]
}
Validating JSON Against a Schema
JavaScript with Ajv
Ajv is the fastest JSON Schema validator for JavaScript:import Ajv from 'ajv';
import addFormats from 'ajv-formats';
const ajv = new Ajv({ allErrors: true }); // collect all errors, not just the first
addFormats(ajv); // adds support for "email", "date-time", "uuid", etc.
const schema = {
type: 'object',
properties: {
name: { type: 'string', minLength: 1 },
email: { type: 'string', format: 'email' },
},
required: ['name', 'email'],
};
const validate = ajv.compile(schema);
const data = { name: 'Alice', email: 'not-an-email' };
if (!validate(data)) {
for (const error of validate.errors) {
console.error(${error.instancePath}: ${error.message});
// Output: "/email: must match format "email""
}
}
Python with jsonschema
import jsonschema
from jsonschema import validate, ValidationError
schema = {
"type": "object",
"properties": {
"name": {"type": "string", "minLength": 1},
"email": {"type": "string", "format": "email"},
},
"required": ["name", "email"],
}
data = {"name": "Alice", "email": "[email protected]"}
try:
validate(instance=data, schema=schema)
print("Valid!")
except ValidationError as e:
print(f"Error at {e.json_path}: {e.message}")
JSON Schema vs. TypeScript Types
If you're using TypeScript, you might wonder why you need JSON Schema at all when you have static types. The key difference:
| TypeScript types | JSON Schema | |
|---|---|---|
| When checked | Compile time | Runtime |
| Works with | Your code | Any JSON, any language |
| API validation | No | Yes |
| Error messages | Compile errors | Runtime error objects |
TypeScript types disappear at runtime. When a user POSTs data to your API, TypeScript can't help you — the types were erased during compilation. JSON Schema validates the actual data at runtime.
The best approach: use TypeScript for type safety in your code and JSON Schema for runtime validation of external data. Libraries like zod generate both from the same source:
import { z } from 'zod';
import { zodToJsonSchema } from 'zod-to-json-schema';
const UserSchema = z.object({
name: z.string().min(1),
email: z.string().email(),
});
// TypeScript type from Zod
type User = z.infer<typeof UserSchema>;
// JSON Schema from Zod (for documentation, OpenAPI, etc.)
const jsonSchema = zodToJsonSchema(UserSchema);
JSON Schema in API Documentation
JSON Schema is the foundation of OpenAPI/Swagger. Every request body and response schema in an OpenAPI spec uses JSON Schema format. Writing clean, precise JSON Schemas directly improves your API documentation, client code generation, and automated testing.
Using the JSON Schema Validator
AI JSONMedic's JSON Validator supports JSON Schema validation — paste your JSON in the top panel, your schema in the Schema tab, and get instant validation with field-level error details.
This is useful for:
- Testing an API response against its documented contract
- Verifying AI-generated JSON matches your expected schema
- Debugging n8n or Zapier workflow output before it hits your database
> Malformed JSON before schema validation? If your JSON won't parse at all (syntax errors, truncated arrays, missing quotes), repair it first with the AI JSONMedic JSON repair tool — it fixes 90+ error patterns so you can then validate against your schema cleanly.
Advanced JSON Schema Keywords
Conditional Schemas: if / then / else
if/then/else lets you apply different constraints depending on a field's value. This is powerful for schemas where required fields change based on a type discriminator:
{
"type": "object",
"properties": {
"paymentMethod": { "type": "string", "enum": ["card", "bank_transfer"] },
"cardNumber": { "type": "string" },
"bankAccount": { "type": "string" }
},
"if": {
"properties": { "paymentMethod": { "const": "card" } }
},
"then": {
"required": ["cardNumber"]
},
"else": {
"required": ["bankAccount"]
}
}
When paymentMethod is "card", cardNumber is required. Otherwise, bankAccount is required. Without if/then/else, you'd need to accept either structure silently and validate manually in code.
oneOf, anyOf, allOf
These keywords apply boolean logic to schemas:
{
"oneOf": [
{ "$ref": "#/$defs/CreditCardPayment" },
{ "$ref": "#/$defs/BankTransferPayment" }
]
}
oneOf— exactly one subschema must match. Use this for discriminated unions.anyOf— one or more subschemas must match. Use this for overlapping types.allOf— all subschemas must match. Use this for schema composition (like extending a base schema).
// allOf for composition: extend a base schema
{
"allOf": [
{ "$ref": "#/$defs/BaseEntity" },
{
"type": "object",
"properties": {
"email": { "type": "string", "format": "email" }
},
"required": ["email"]
}
]
}
A common pitfall: oneOf is strict and validates the entire document against each subschema. For large schemas this is slow and error messages can be confusing ("data must match exactly one schema in oneOf" without telling you which constraint failed). Consider anyOf or a discriminator property for better developer experience.
Schema Reuse Patterns
Large API surfaces have dozens of schemas sharing common structures. Use these patterns to avoid repetition:
Centralized Definitions with $defs
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://example.com/schemas/common.json",
"$defs": {
"Timestamp": {
"type": "string",
"format": "date-time",
"description": "ISO 8601 date-time string"
},
"UUID": {
"type": "string",
"format": "uuid"
},
"Pagination": {
"type": "object",
"properties": {
"page": { "type": "integer", "minimum": 1 },
"perPage": { "type": "integer", "minimum": 1, "maximum": 100 },
"total": { "type": "integer", "minimum": 0 }
},
"required": ["page", "perPage", "total"]
}
}
}
Other schemas reference this file:
{
"$ref": "https://example.com/schemas/common.json#/$defs/Pagination"
}
Schema Composition for Inheritance
{
"$defs": {
"BaseEntity": {
"type": "object",
"properties": {
"id": { "$ref": "#/$defs/UUID" },
"createdAt": { "$ref": "#/$defs/Timestamp" },
"updatedAt": { "$ref": "#/$defs/Timestamp" }
},
"required": ["id", "createdAt", "updatedAt"]
},
"User": {
"allOf": [
{ "$ref": "#/$defs/BaseEntity" },
{
"type": "object",
"properties": {
"name": { "type": "string" },
"email": { "type": "string", "format": "email" }
},
"required": ["name", "email"]
}
]
}
}
}
Integrating JSON Schema with OpenAPI
OpenAPI 3.x uses JSON Schema (with minor extensions) for all request and response body definitions. Writing JSON Schema directly maps to OpenAPI spec sections.
Request Body Schema
# openapi.yaml
paths:
/users:
post:
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateUserRequest'
components:
schemas:
CreateUserRequest:
type: object
properties:
name:
type: string
minLength: 1
maxLength: 100
email:
type: string
format: email
role:
type: string
enum: [admin, user, viewer]
default: user
required:
- name
- email
The same schema works for:
- Swagger UI documentation — rendered as interactive docs
- Server-side validation — frameworks like FastAPI, NestJS, and Hono validate requests automatically against the schema
- Client code generation — tools like
openapi-typescriptgenerate TypeScript types from these schemas
Exporting from Code to OpenAPI
If you define schemas in code first (with Zod or similar), export them to OpenAPI:
import { z } from 'zod';
import { zodToJsonSchema } from 'zod-to-json-schema';
import { generateOpenApi } from '@ts-rest/open-api';
const CreateUserSchema = z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
role: z.enum(['admin', 'user', 'viewer']).default('user'),
});
// Export as JSON Schema (for OpenAPI components/schemas)
const jsonSchema = zodToJsonSchema(CreateUserSchema, 'CreateUserRequest');
Schema Evolution and Versioning
Schemas change as APIs evolve. Managing that change without breaking consumers requires a deliberate strategy.
Non-Breaking Changes (Safe to Deploy)
- Adding new optional properties
- Widening a type (e.g.,
string→string | null) - Relaxing constraints (e.g., raising
maxLength)
Breaking Changes (Require a Version Bump)
- Removing required properties
- Renaming properties
- Narrowing a type (e.g.,
string | number→string) - Tightening constraints (e.g., lowering
maxLength) - Changing
additionalPropertiesfromtruetofalse
Schema Versioning in Practice
Use $id to version your schemas:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://example.com/schemas/user/v2.json",
"type": "object",
"properties": {
"id": { "type": "string", "format": "uuid" },
"firstName": { "type": "string" },
"lastName": { "type": "string" }
},
"required": ["id", "firstName", "lastName"]
}
Store both v1.json and v2.json. During a migration period, validate against both and accept either. Once all clients have migrated, retire v1.json.
Generating Schemas from TypeScript Types
Rather than maintaining JSON Schema files separately from TypeScript interfaces, generate one from the other. This ensures they stay in sync.
Option 1: Zod (Schema-First)
Define validation logic in Zod and derive both the TypeScript type and JSON Schema from it:
import { z } from 'zod';
import { zodToJsonSchema } from 'zod-to-json-schema';
const ProductSchema = z.object({
id: z.string().uuid(),
name: z.string().min(1).max(200),
price: z.number().positive(),
tags: z.array(z.string()).max(10).default([]),
});
// TypeScript type — used in your code
type Product = z.infer<typeof ProductSchema>;
// JSON Schema — used in OpenAPI, documentation, and cross-language validation
const productJsonSchema = zodToJsonSchema(ProductSchema, {
name: 'Product',
$refStrategy: 'none',
});
Option 2: ts-json-schema-generator (Type-First)
If you already have TypeScript interfaces and want to generate JSON Schemas from them without rewriting in Zod:
npx ts-json-schema-generator --path src/types.ts --type User --out schemas/user.json
// src/types.ts
export interface User {
id: number;
name: string;
/* @format email /
email: string;
role: 'admin' | 'user' | 'viewer';
tags?: string[];
}
JSDoc annotations like /* @format email / map directly to JSON Schema keywords, giving you fine-grained control from TypeScript source comments.
Keeping Schema and Types in Sync
Add a CI step that regenerates the JSON Schema from your TypeScript types and fails the build if the output differs from the committed file:
# In CI
npx ts-json-schema-generator --path src/types.ts --type User --out /tmp/user.schema.json
diff schemas/user.json /tmp/user.schema.json || (echo "Schema out of sync with types" && exit 1)
FAQ
What is JSON Schema used for?
JSON Schema defines the expected structure of a JSON document: types, required fields, value constraints, and nested shapes. It's used for API request/response validation, form input validation, OpenAPI documentation, code generation, and automated testing.
What's the difference between oneOf and anyOf in JSON Schema?
oneOf requires exactly one subschema to match — if more than one matches, validation fails. anyOf requires at least one subschema to match and succeeds if multiple match. Use oneOf for strict discriminated unions and anyOf when overlapping schemas are acceptable.
How do I reuse schemas across multiple JSON Schema files?
Use $defs to define reusable schemas within a file, and $ref with a full URI to reference schemas across files. Store common definitions (like Timestamp, UUID, Pagination) in a shared common.json file with a stable $id URL.
Can JSON Schema validate data at runtime in JavaScript?
Yes. Libraries like Ajv compile a JSON Schema into a validator function that you call with your data. It returns true/false plus detailed error objects describing which fields failed and why. Ajv is the standard choice for Node.js and browser environments.
How do I generate TypeScript types from a JSON Schema?
Use json-schema-to-typescript (npm install json-schema-to-typescript) to generate TypeScript interfaces from JSON Schema files. Alternatively, use Zod as your single source of truth and derive both the JSON Schema and TypeScript type from the same Zod schema definition.
Does JSON Schema work with OpenAPI?
Yes. OpenAPI 3.x uses JSON Schema (with minor extensions) for all request body and response schema definitions. Any valid JSON Schema can be used inside an OpenAPI components/schemas section, and tools like Swagger UI, Redoc, and code generators all consume these schemas directly.
What does "1 validation error for [ModelName]" mean?
This error comes from Pydantic (Python). It means one field in your data failed validation against the model's schema. The full message shows which field failed, the expected type, and the actual value. For example: 1 validation error for UserResponse / email / value is not a valid email address. Fix it by checking the field path in the error, correcting the data type or format, and re-running validation. If the JSON itself is malformed before Pydantic even sees it, repair it first with AI JSONMedic — Pydantic can only validate structurally valid JSON.
Why does my LLM response fail JSON Schema validation even with structured output mode?
Structured output mode (OpenAI's strict: true, Anthropic's output_config, Gemini's response_schema) reduces failures but doesn't eliminate them. Common remaining causes: (1) the model truncates the response at max_tokens — you get valid JSON that's incomplete relative to your schema; (2) constrained decoding handles basic types but struggles with complex oneOf/anyOf schemas; (3) the schema itself has an error the model can't satisfy. Fix approach: increase max_tokens, simplify the schema, and always wrap responses in a try/catch that repairs and retries before raising.
How do I fix a JSON Schema validation error when the JSON itself is malformed?
Schema validation requires syntactically valid JSON first. If JSON.parse() throws before your schema validator even runs, the JSON has syntax errors (missing quotes, trailing commas, truncated output). Fix the syntax errors first — either manually or with AI JSONMedic's repair tool — then run schema validation on the clean output. The two-step workflow is: repair → validate → handle schema errors.
Still dealing with broken JSON?
Paste it in and get it fixed in under 1 second — free, no signup, no install. Works with ChatGPT, Claude, n8n, and any AI output.
Fix My JSON Free →Related Articles