Why JSON Schema?
Every time you accept JSON from an external source — API requests, config files, form submissions, webhooks — you need to validate it. Without validation, a missing field or wrong type can cascade into subtle bugs, corrupted data, or security vulnerabilities.
API Contracts
Validate request/response bodies between services
Form Validation
Server-side validation of user input
Config Files
Ensure config schemas are correct at startup
Documentation
Self-documenting data shapes for teams
Code Generation
Generate types from schemas (OpenAPI, Zod)
Testing
Contract testing between microservices
Your First Schema
A JSON Schema is just a JSON object that describes another JSON object. Start with the simplest valid schema:
1{2 "$schema": "https://json-schema.org/draft/2020-12/schema",3 "title": "User",4 "description": "A registered user account",5 "type": "object",6 "properties": {7 "id": {8 "type": "integer",9 "description": "Unique user identifier"10 },11 "name": {12 "type": "string",13 "minLength": 1,14 "maxLength": 10015 },16 "email": {17 "type": "string",18 "format": "email"19 },20 "age": {21 "type": "integer",22 "minimum": 0,23 "maximum": 15024 },25 "active": {26 "type": "boolean",27 "default": true28 }29 },30 "required": ["id", "name", "email"],31 "additionalProperties": false32}Tip
required array lists fields that must be present. additionalProperties: false rejects unknown fields — useful for strict API validation.Warning
default keyword (used on active above) is not enforced during validation by most libraries, including Ajv. It serves as documentation only unless you explicitly enable it — for example, by passing useDefaults: true to Ajv.All Schema Keywords
Type Constraints
| Keyword | Applies To | Example |
|---|---|---|
| type | All | "type": "string" |
| enum | All | "enum": ["active", "inactive"] |
| const | All | "const": "v2" |
| minLength / maxLength | String | "minLength": 1 |
| pattern | String | "pattern": "^[A-Z]{2}$" |
| format | String | "format": "email" |
| minimum / maximum | Number | "minimum": 0 |
| exclusiveMinimum | Number | "exclusiveMinimum": 0 |
| multipleOf | Number | "multipleOf": 0.01 |
| minItems / maxItems | Array | "minItems": 1 |
| uniqueItems | Array | "uniqueItems": true |
| items | Array | "items": { "type": "string" } |
| properties | Object | Define per-key schemas |
| required | Object | "required": ["id"] |
| additionalProperties | Object | "additionalProperties": false |
String Formats
The format keyword validates common string patterns:
| Format | Example Value |
|---|---|
| [email protected] | |
| uri | https://example.com/api |
| date | 2026-03-28 |
| date-time | 2026-03-28T14:30:00Z |
| uuid | 550e8400-e29b-41d4-a716-446655440000 |
| ipv4 | 192.168.1.1 |
| hostname | api.example.com |
Nested Object Schemas
1{2 "type": "object",3 "properties": {4 "name": { "type": "string" },5 "address": {6 "type": "object",7 "properties": {8 "street": { "type": "string" },9 "city": { "type": "string" },10 "zip": {11 "type": "string",12 "pattern": "^[0-9]{5}(-[0-9]{4})?$"13 }14 },15 "required": ["street", "city"]16 }17 },18 "required": ["name", "address"]19}Array Schemas
1{2 "type": "object",3 "properties": {4 "tags": {5 "type": "array",6 "items": { "type": "string", "minLength": 1 },7 "minItems": 1,8 "maxItems": 10,9 "uniqueItems": true10 },11 "scores": {12 "type": "array",13 "items": { "type": "number", "minimum": 0, "maximum": 100 }14 }15 }16}Composition Keywords
Combine schemas using logical operators:
1{2 "allOf": [3 {4 "type": "object",5 "properties": {6 "id": { "type": "integer" }7 },8 "required": ["id"]9 },10 {11 "type": "object",12 "properties": {13 "timestamp": { "type": "string", "format": "date-time" }14 },15 "required": ["timestamp"]16 }17 ]18}Reusable Schemas with $ref
Use $defs to define reusable sub-schemas and $ref to reference them:
1{2 "$schema": "https://json-schema.org/draft/2020-12/schema",3 "type": "object",4 "properties": {5 "billing": { "$ref": "#/$defs/address" },6 "shipping": { "$ref": "#/$defs/address" }7 },8 "$defs": {9 "address": {10 "type": "object",11 "properties": {12 "street": { "type": "string" },13 "city": { "type": "string" },14 "country": { "type": "string", "minLength": 2, "maxLength": 2 }15 },16 "required": ["street", "city", "country"]17 }18 }19}Validating with JavaScript (Ajv)
1import Ajv from 'ajv';2import addFormats from 'ajv-formats';34const ajv = new Ajv({ allErrors: true });5addFormats(ajv);67const schema = {8 type: 'object',9 properties: {10 email: { type: 'string', format: 'email' },11 age: { type: 'integer', minimum: 18 },12 },13 required: ['email'],14 additionalProperties: false,15};1617const validate = ajv.compile(schema);1819function validateUser(data) {20 const valid = validate(data);21 if (!valid) {22 return {23 ok: false,24 errors: validate.errors.map(e => ({25 field: e.instancePath || '/',26 message: e.message,27 })),28 };29 }30 return { ok: true };31}3233validateUser({ email: 'bad', age: 15 });34// { ok: false, errors: [35// { field: "/email", message: "must match format "email"" },36// { field: "/age", message: "must be >= 18" }37// ]}Conditional Schemas
Apply different validation rules based on the value of another field:
1{2 "type": "object",3 "properties": {4 "payment_type": { "enum": ["card", "bank_transfer"] },5 "card_number": { "type": "string" },6 "iban": { "type": "string" }7 },8 "required": ["payment_type"],9 "if": {10 "properties": { "payment_type": { "const": "card" } }11 },12 "then": {13 "required": ["card_number"]14 },15 "else": {16 "required": ["iban"]17 }18}Try These Tools
Schema Draft Comparison
| Feature | Draft-04 | Draft-07 | 2020-12 |
|---|---|---|---|
| if/then/else | ✓ | ✓ | |
| $defs (definitions) | ✓ (definitions) | ✓ (definitions) | ✓ ($defs) |
| $ref alongside keywords | ✓ | ||
| Vocabulary system | ✓ | ||
| prefixItems (tuples) | ✓ | ||
| Format as assertion | ✓ | Optional | Optional |
Try It Yourself
Write a JSON object that matches this schema: an object with name (string, required), age (integer, min 0), and email (string, required).
Try It Yourself
Edit to experiment — the parser will tell you if the JSON is valid