Learn/Practical Guides

JSON Schema — Validate API Data Like a Pro

JSON Schema lets you describe the shape, types, and constraints of your data — then validate automatically. Think of it as TypeScript for runtime data. This guide takes you from zero to production-ready schemas.

Intermediate~18 min read

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:

Basic user schemajson
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": 100
15 },
16 "email": {
17 "type": "string",
18 "format": "email"
19 },
20 "age": {
21 "type": "integer",
22 "minimum": 0,
23 "maximum": 150
24 },
25 "active": {
26 "type": "boolean",
27 "default": true
28 }
29 },
30 "required": ["id", "name", "email"],
31 "additionalProperties": false
32}

Tip

The required array lists fields that must be present. additionalProperties: false rejects unknown fields — useful for strict API validation.

Warning

The 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

KeywordApplies ToExample
typeAll"type": "string"
enumAll"enum": ["active", "inactive"]
constAll"const": "v2"
minLength / maxLengthString"minLength": 1
patternString"pattern": "^[A-Z]{2}$"
formatString"format": "email"
minimum / maximumNumber"minimum": 0
exclusiveMinimumNumber"exclusiveMinimum": 0
multipleOfNumber"multipleOf": 0.01
minItems / maxItemsArray"minItems": 1
uniqueItemsArray"uniqueItems": true
itemsArray"items": { "type": "string" }
propertiesObjectDefine per-key schemas
requiredObject"required": ["id"]
additionalPropertiesObject"additionalProperties": false

String Formats

The format keyword validates common string patterns:

FormatExample Value
email[email protected]
urihttps://example.com/api
date2026-03-28
date-time2026-03-28T14:30:00Z
uuid550e8400-e29b-41d4-a716-446655440000
ipv4192.168.1.1
hostnameapi.example.com

Nested Object Schemas

Schema with nested addressjson
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

Array of typed itemsjson
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": true
10 },
11 "scores": {
12 "type": "array",
13 "items": { "type": "number", "minimum": 0, "maximum": 100 }
14 }
15 }
16}

Composition Keywords

Combine schemas using logical operators:

Schema Composition
Example: allOf for combining constraintsjson
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:

DRY schemas with $refjson
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)

Server-side validation with Ajvjavascript
1import Ajv from 'ajv';
2import addFormats from 'ajv-formats';
3
4const ajv = new Ajv({ allErrors: true });
5addFormats(ajv);
6
7const 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};
16
17const validate = ajv.compile(schema);
18
19function 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}
32
33validateUser({ 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:

if/then/else for conditional validationjson
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}

Schema Draft Comparison

FeatureDraft-04Draft-072020-12
if/then/else
$defs (definitions)✓ (definitions)✓ (definitions)✓ ($defs)
$ref alongside keywords
Vocabulary system
prefixItems (tuples)
Format as assertionOptionalOptional

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

Frequently Asked Questions

What is JSON Schema?
JSON Schema is a vocabulary that lets you describe the structure of JSON data. It is itself written in JSON and can validate that other JSON documents conform to specified types, formats, ranges, and patterns.
Which JSON Schema draft should I use?
For new projects, use Draft 2020-12 — it is the latest stable version with the best feature set. Older drafts (Draft-07, Draft-04) are still widely supported if you need backward compatibility.
How do I validate JSON against a schema in JavaScript?
Use the Ajv library: npm install ajv ajv-formats. Create a validator with const validate = ajv.compile(schema), then call validate(data). Errors are in validate.errors.
Can JSON Schema validate nested objects?
Yes. Use the "properties" keyword at each nesting level. You can also use "$ref" to reference reusable sub-schemas to keep definitions DRY.
What is the difference between JSON Schema and TypeScript types?
TypeScript types exist only at compile time and are erased at runtime. JSON Schema validates data at runtime — in APIs, form submissions, config loading, and database writes.