JSON Schema Validation: Complete Guide with Examples
What Is JSON Schema?
JSON Schema is a declarative language for describing the structure and constraints of JSON data. Think of it as TypeScript types for your JSON -- it defines what fields exist, what types they have, which ones are required, and what values are valid. But unlike TypeScript, JSON Schema validation happens at runtime, which makes it perfect for validating API payloads, configuration files, form submissions, and any data that crosses trust boundaries.
A JSON Schema is itself a JSON document. It describes another JSON document. When you validate data against a schema, the validator checks every constraint and returns a list of violations if the data does not conform. This approach is language-agnostic -- the same schema works in JavaScript, Python, Go, Java, C#, and every other language with a JSON Schema library.
// A simple JSON Schema
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"name": { "type": "string", "minLength": 1 },
"email": { "type": "string", "format": "email" },
"age": { "type": "integer", "minimum": 0, "maximum": 150 }
},
"required": ["name", "email"]
}
// Valid data:
{ "name": "Alice", "email": "[email protected]", "age": 30 }
// Invalid data (missing required field, wrong type):
{ "email": 123 }
// Errors: "name" is required, "email" must be a stringTo experiment with JSON structures before writing schemas, format your data with our JSON Formatter -- it validates syntax, highlights errors, and pretty-prints with configurable indentation.
Type System: The Foundation
Every JSON Schema starts with a type declaration. JSON Schema supports seven primitive types that map directly to JSON value types.
| Type | JSON Value | Example |
|---|---|---|
| string | Text in double quotes | "hello" |
| number | Any numeric value | 3.14, -1, 2e10 |
| integer | Whole numbers only | 42, -7, 0 |
| boolean | true or false | true |
| object | Key-value pairs | {"key": "value"} |
| array | Ordered list | [1, 2, 3] |
| null | Null value | null |
// Allow multiple types (nullable string)
{ "type": ["string", "null"] }
// Accepts: "hello", null
// Rejects: 42, true
// Enum: restrict to specific values
{ "type": "string", "enum": ["active", "inactive", "pending"] }
// Const: restrict to exactly one value
{ "const": "production" }String Constraints
Strings support length constraints, regex pattern matching, and built-in format validators. The format keyword provides semantic validation for common string types like emails, URIs, dates, and UUIDs.
// String with length and pattern constraints
{
"type": "string",
"minLength": 3,
"maxLength": 50,
"pattern": "^[a-zA-Z][a-zA-Z0-9_]*$"
}
// Accepts: "username_123"
// Rejects: "ab" (too short), "123abc" (doesn't start with letter)
// Built-in format validators
{ "type": "string", "format": "email" } // RFC 5321 email
{ "type": "string", "format": "uri" } // Full URI with scheme
{ "type": "string", "format": "date" } // ISO 8601 date (YYYY-MM-DD)
{ "type": "string", "format": "date-time" } // ISO 8601 datetime
{ "type": "string", "format": "uuid" } // UUID any version
{ "type": "string", "format": "ipv4" } // IPv4 address
{ "type": "string", "format": "ipv6" } // IPv6 address
{ "type": "string", "format": "hostname" } // Internet hostname
{ "type": "string", "format": "json-pointer" } // JSON Pointer (RFC 6901)The pattern keyword accepts any valid regular expression. For building and testing regex patterns, use our Regex Tester to verify they match your expected inputs before embedding them in schemas.
Number Constraints
// Integer with range
{
"type": "integer",
"minimum": 1,
"maximum": 100
}
// Number with exclusive bounds
{
"type": "number",
"exclusiveMinimum": 0, // greater than 0 (not equal)
"exclusiveMaximum": 1 // less than 1 (not equal)
}
// Accepts: 0.5, 0.001, 0.999
// Rejects: 0, 1, -0.5
// Multiple of (useful for prices, grid sizes)
{
"type": "number",
"multipleOf": 0.01 // two decimal places max
}
// Accepts: 9.99, 10.00, 0.01
// Rejects: 9.999
// Percentage with step
{
"type": "integer",
"minimum": 0,
"maximum": 100,
"multipleOf": 5 // 0, 5, 10, 15, ..., 100
}Object Schemas: Properties, Required Fields, and More
Objects are the most common type in JSON APIs. JSON Schema gives you fine-grained control over which properties are allowed, which are required, and what additional properties look like.
// Complete API request schema
{
"type": "object",
"properties": {
"username": {
"type": "string",
"minLength": 3,
"maxLength": 30,
"pattern": "^[a-zA-Z0-9_]+$"
},
"email": {
"type": "string",
"format": "email"
},
"role": {
"type": "string",
"enum": ["admin", "editor", "viewer"],
"default": "viewer"
},
"metadata": {
"type": "object",
"additionalProperties": { "type": "string" }
}
},
"required": ["username", "email"],
"additionalProperties": false
}
// "additionalProperties": false → rejects any field not listed in "properties"
// "additionalProperties": true → allows any extra fields (default behavior)
// "additionalProperties": { "type": "string" } → extra fields must be stringsProperty Name Patterns
// patternProperties: apply schemas based on property name patterns
{
"type": "object",
"patternProperties": {
"^x-": { "type": "string" }, // x-* headers must be strings
"^[0-9]+$": { "type": "boolean" } // numeric keys must be booleans
},
"additionalProperties": false
}
// Accepts: { "x-custom": "value", "123": true }
// Rejects: { "x-custom": 42 } (x-* must be string)Property Dependencies
// dependentRequired: if field A exists, field B is required
{
"type": "object",
"properties": {
"creditCard": { "type": "string" },
"billingAddress": { "type": "string" },
"couponCode": { "type": "string" }
},
"dependentRequired": {
"creditCard": ["billingAddress"] // creditCard requires billingAddress
}
}
// Accepts: { "couponCode": "SAVE10" } (no card, no address needed)
// Accepts: { "creditCard": "4111...", "billingAddress": "123 St" }
// Rejects: { "creditCard": "4111..." } (missing billingAddress)Array Schemas: Items, Tuples, and Uniqueness
// Array of strings with length constraints
{
"type": "array",
"items": { "type": "string", "minLength": 1 },
"minItems": 1,
"maxItems": 10,
"uniqueItems": true
}
// Accepts: ["apple", "banana", "cherry"]
// Rejects: ["apple", "apple"] (not unique)
// Rejects: [] (minItems is 1)
// Tuple validation (Draft 2020-12: prefixItems)
{
"type": "array",
"prefixItems": [
{ "type": "string" }, // first element: string
{ "type": "integer" }, // second element: integer
{ "type": "boolean" } // third element: boolean
],
"items": false // no additional items allowed
}
// Accepts: ["hello", 42, true]
// Rejects: ["hello", 42, true, "extra"]
// Mixed array (allow strings or numbers)
{
"type": "array",
"items": {
"oneOf": [
{ "type": "string" },
{ "type": "number" }
]
}
}
// Accepts: ["hello", 42, "world", 3.14]
// Rejects: ["hello", true] (boolean not allowed)
// Array of objects (common API pattern)
{
"type": "array",
"items": {
"type": "object",
"properties": {
"id": { "type": "integer" },
"name": { "type": "string" }
},
"required": ["id", "name"]
}
}Composition: allOf, anyOf, oneOf, not
Composition keywords let you combine schemas using logical operators. This is how you model inheritance, polymorphism, and complex validation rules that cannot be expressed with simple property definitions.
| Keyword | Logic | Use Case |
|---|---|---|
| allOf | AND -- must match all schemas | Inheritance, combining constraints |
| anyOf | OR -- must match at least one | Multiple acceptable formats |
| oneOf | XOR -- must match exactly one | Discriminated unions, polymorphism |
| not | NOT -- must not match | Exclusion, blacklisting values |
// allOf: extend a base schema
{
"allOf": [
{
"type": "object",
"properties": {
"id": { "type": "integer" },
"createdAt": { "type": "string", "format": "date-time" }
},
"required": ["id", "createdAt"]
},
{
"properties": {
"name": { "type": "string" },
"email": { "type": "string", "format": "email" }
},
"required": ["name", "email"]
}
]
}
// Result: object must have id, createdAt, name, AND email
// oneOf: discriminated union (API event types)
{
"oneOf": [
{
"type": "object",
"properties": {
"type": { "const": "user.created" },
"data": {
"type": "object",
"properties": { "userId": { "type": "integer" } },
"required": ["userId"]
}
},
"required": ["type", "data"]
},
{
"type": "object",
"properties": {
"type": { "const": "order.completed" },
"data": {
"type": "object",
"properties": { "orderId": { "type": "string" } },
"required": ["orderId"]
}
},
"required": ["type", "data"]
}
]
}Conditional Schemas: if / then / else
Conditional schemas let you apply different validation rules based on the data itself. This is extremely useful for forms and API payloads where the required fields depend on a type or mode selection.
// If paymentMethod is "credit_card", require cardNumber and cvv
// If paymentMethod is "bank_transfer", require accountNumber and routingNumber
{
"type": "object",
"properties": {
"paymentMethod": { "type": "string", "enum": ["credit_card", "bank_transfer"] },
"amount": { "type": "number", "minimum": 0.01 }
},
"required": ["paymentMethod", "amount"],
"if": {
"properties": { "paymentMethod": { "const": "credit_card" } }
},
"then": {
"properties": {
"cardNumber": { "type": "string", "pattern": "^[0-9]{16}$" },
"cvv": { "type": "string", "pattern": "^[0-9]{3,4}$" }
},
"required": ["cardNumber", "cvv"]
},
"else": {
"properties": {
"accountNumber": { "type": "string" },
"routingNumber": { "type": "string", "pattern": "^[0-9]{9}$" }
},
"required": ["accountNumber", "routingNumber"]
}
}
// Valid credit card payment:
{ "paymentMethod": "credit_card", "amount": 99.99, "cardNumber": "4111111111111111", "cvv": "123" }
// Valid bank transfer:
{ "paymentMethod": "bank_transfer", "amount": 500, "accountNumber": "12345678", "routingNumber": "021000021" }
// Invalid (credit card selected but missing cvv):
{ "paymentMethod": "credit_card", "amount": 99.99, "cardNumber": "4111111111111111" }Reusable Schemas with $ref and $defs
Real-world schemas share definitions across endpoints. The $defs keyword (formerly definitions in Draft-07) stores reusable schemas, and $ref references them. This keeps your schemas DRY and maintainable.
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$defs": {
"address": {
"type": "object",
"properties": {
"street": { "type": "string" },
"city": { "type": "string" },
"state": { "type": "string", "pattern": "^[A-Z]{2}$" },
"zip": { "type": "string", "pattern": "^[0-9]{5}(-[0-9]{4})?$" }
},
"required": ["street", "city", "state", "zip"]
},
"phone": {
"type": "string",
"pattern": "^\\+?[1-9]\\d{6,14}$"
}
},
"type": "object",
"properties": {
"name": { "type": "string" },
"homeAddress": { "$ref": "#/$defs/address" },
"workAddress": { "$ref": "#/$defs/address" },
"phone": { "$ref": "#/$defs/phone" }
},
"required": ["name", "homeAddress"]
}
// Both homeAddress and workAddress validate against the same schema
// Changes to the address definition automatically apply everywhereFor schemas that span multiple files, you can use absolute or relative URIs in $ref. For example, {"$ref": "./address.schema.json"} references a separate file. When working with complex JSON structures, use our JSON Path Tester to navigate and extract data from deeply nested documents.
Validation in JavaScript with Ajv
Ajv (Another JSON Schema Validator) is the most popular JSON Schema library for JavaScript and TypeScript. It compiles schemas into optimized validation functions for maximum performance.
// Installation
// npm install ajv ajv-formats
import Ajv from 'ajv';
import addFormats from 'ajv-formats';
const ajv = new Ajv({ allErrors: true });
addFormats(ajv); // adds "email", "uri", "date-time", etc.
const schema = {
type: 'object',
properties: {
name: { type: 'string', minLength: 1 },
email: { type: 'string', format: 'email' },
age: { type: 'integer', minimum: 0 },
},
required: ['name', 'email'],
additionalProperties: false,
};
const validate = ajv.compile(schema);
// Valid data
const valid = validate({ name: 'Alice', email: '[email protected]', age: 30 });
console.log(valid); // true
// Invalid data
const invalid = validate({ email: 'not-an-email' });
console.log(invalid); // false
console.log(validate.errors);
// [
// { keyword: 'required', params: { missingProperty: 'name' } },
// { keyword: 'format', params: { format: 'email' } }
// ]
// Express.js middleware example
function validateBody(schema) {
const validate = ajv.compile(schema);
return (req, res, next) => {
if (!validate(req.body)) {
return res.status(400).json({
error: 'Validation failed',
details: validate.errors,
});
}
next();
};
}
app.post('/api/users', validateBody(schema), (req, res) => {
// req.body is guaranteed to be valid here
});Validation in Python with jsonschema
# pip install jsonschema[format]
from jsonschema import validate, ValidationError, Draft202012Validator
schema = {
"type": "object",
"properties": {
"name": {"type": "string", "minLength": 1},
"email": {"type": "string", "format": "email"},
"tags": {
"type": "array",
"items": {"type": "string"},
"uniqueItems": True
}
},
"required": ["name", "email"],
"additionalProperties": False
}
# Simple validation (raises on first error)
try:
validate(instance={"name": "", "email": "bad"}, schema=schema)
except ValidationError as e:
print(f"Error: {e.message}")
# Error: '' is too short
# Collect all errors
validator = Draft202012Validator(schema)
errors = list(validator.iter_errors({"email": 123}))
for error in errors:
print(f"{error.json_path}: {error.message}")
# $.name: 'name' is a required property
# $.email: 123 is not of type 'string'
# FastAPI integration (uses JSON Schema internally)
from pydantic import BaseModel, EmailStr
class UserCreate(BaseModel):
name: str
email: EmailStr
tags: list[str] = []
# Pydantic generates JSON Schema automatically:
print(UserCreate.model_json_schema())Validation in Go with gojsonschema
// go get github.com/xeipuuv/gojsonschema
package main
import (
"fmt"
"github.com/xeipuuv/gojsonschema"
)
func main() {
schema := gojsonschema.NewStringLoader(`{
"type": "object",
"properties": {
"name": {"type": "string", "minLength": 1},
"email": {"type": "string", "format": "email"}
},
"required": ["name", "email"],
"additionalProperties": false
}`)
document := gojsonschema.NewStringLoader(`{
"name": "Alice",
"email": "[email protected]"
}`)
result, err := gojsonschema.Validate(schema, document)
if err != nil {
panic(err)
}
if result.Valid() {
fmt.Println("Document is valid")
} else {
for _, err := range result.Errors() {
fmt.Printf("- %s\n", err)
}
}
}Real-World Schema: REST API Response
Here is a complete schema for a paginated API response -- the kind you would use to validate responses from a REST API.
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$defs": {
"user": {
"type": "object",
"properties": {
"id": { "type": "integer", "minimum": 1 },
"username": { "type": "string", "minLength": 3, "maxLength": 30 },
"email": { "type": "string", "format": "email" },
"role": { "type": "string", "enum": ["admin", "editor", "viewer"] },
"createdAt": { "type": "string", "format": "date-time" },
"profile": {
"type": "object",
"properties": {
"displayName": { "type": "string" },
"bio": { "type": ["string", "null"], "maxLength": 500 },
"avatarUrl": { "type": ["string", "null"], "format": "uri" }
},
"required": ["displayName"]
}
},
"required": ["id", "username", "email", "role", "createdAt"]
},
"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"]
}
},
"type": "object",
"properties": {
"data": {
"type": "array",
"items": { "$ref": "#/$defs/user" }
},
"pagination": { "$ref": "#/$defs/pagination" }
},
"required": ["data", "pagination"]
}This schema validates the entire response structure including nested user profiles and pagination metadata. For designing APIs with correct status codes, check our API Response Codes Best Practices guide and the HTTP Status Codes reference.
Schema Generation from TypeScript
If you already have TypeScript interfaces, you can generate JSON Schema automatically instead of writing it by hand. This keeps your types and validation in sync.
// Install: npm install typescript-json-schema -D
// Define your TypeScript interface
interface User {
/** @minimum 1 */
id: number;
/** @minLength 3 @maxLength 30 */
username: string;
/** @format email */
email: string;
role: 'admin' | 'editor' | 'viewer';
/** @format date-time */
createdAt: string;
}
// Generate schema from CLI:
// npx typescript-json-schema tsconfig.json User --required --strictNullChecks
// Or use Zod (runtime validation + schema generation):
import { z } from 'zod';
import { zodToJsonSchema } from 'zod-to-json-schema';
const userSchema = z.object({
id: z.number().int().min(1),
username: z.string().min(3).max(30),
email: z.string().email(),
role: z.enum(['admin', 'editor', 'viewer']),
createdAt: z.string().datetime(),
});
const jsonSchema = zodToJsonSchema(userSchema);
console.log(JSON.stringify(jsonSchema, null, 2));Need to convert JSON data to TypeScript types first? Our JSON to TypeScript converter generates accurate type definitions from any JSON payload. For converting between JSON and other formats, see our JSON to YAML and JSON to XML converters.
Common Mistakes and Best Practices
- Forgetting additionalProperties -- By default, objects allow any extra fields. Set
additionalProperties: falseto catch typos and unexpected data in API requests. - Not enabling format validation -- The
formatkeyword is an annotation by default in most validators. In Ajv, installajv-formatsand calladdFormats(ajv). In jsonschema (Python), install withpip install jsonschema[format]. - Using oneOf when anyOf works --
oneOfrequires exactly one match. If schemas overlap, validation fails even though the data is valid. UseanyOfunless you specifically need exclusive matching. - Overly strict patterns -- Regex patterns in schemas should validate structure, not content. A pattern like
^[A-Z][a-z]+$for names rejects valid names like "O'Brien" or "McDonald". - Not using $defs for repeated structures -- Duplicating schemas leads to inconsistencies when you update one copy but forget the others. Extract shared definitions into
$defsand reference them with$ref. - Ignoring error messages -- Default validation errors are technical. Map them to user-friendly messages in your API layer, especially for form validation.
Draft Version Comparison
| Feature | Draft-07 | 2019-09 | 2020-12 |
|---|---|---|---|
| if/then/else | Yes | Yes | Yes |
| $defs (was definitions) | definitions | $defs | $defs |
| prefixItems (tuples) | No | No | Yes |
| dependentRequired | No | Yes | Yes |
| $dynamicRef | No | No | Yes |
| unevaluatedProperties | No | Yes | Yes |
Frequently Asked Questions
What is JSON Schema and why should I use it?
JSON Schema is a declarative language for describing the structure and constraints of JSON data. It defines allowed types, required fields, string formats, and nested structures. Use it to validate API payloads, configuration files, form data, and any JSON crossing trust boundaries. It prevents invalid data from entering your system and serves as living documentation for data contracts. The same schema works across all programming languages.
Which JSON Schema draft version should I use?
Use Draft 2020-12 for new projects. It is the latest stable specification supported by major validators including Ajv, jsonschema, and gojsonschema. It introduced prefixItems for tuples and replaced definitions with $defs. If maintaining existing schemas, Draft-07 remains widely supported and perfectly adequate for most use cases.
Can JSON Schema validate nested objects and arrays?
Yes, JSON Schema validates deeply nested structures. Use properties for object fields, items for array elements, and additionalProperties to control extra fields. Nest schemas to any depth, use $ref for shared definitions, and apply conditional validation with if/then/else. Arrays support minItems, maxItems, uniqueItems, and prefixItems for tuple-style validation.
Format and Validate Your JSON
Paste any JSON into our free formatter to validate syntax, pretty-print with custom indentation, and catch errors before writing your schema.
Open JSON FormatterRelated Articles
JSON Formatting Guide
Learn proper JSON indentation, validation, and common syntax errors.
JSON vs YAML vs XML
Compare data formats: pros, cons, readability, and ecosystem support.
API Response Codes Best Practices
HTTP status codes for REST APIs: 4xx vs 5xx, error formats, and patterns.
Regex Patterns Cheat Sheet
30+ copy-paste regex patterns for validation and data extraction.