What Is JSON Schema?
JSON Schema is a JSON document that describes the expected structure of another JSON document. It defines types, required fields, value constraints, and nested shapes. A validator reads both the schema and the input and tells you: valid or not, and exactly why not.
The spec lives at json-schema.org. There have been several draft versions; Draft 07 is the most widely supported by libraries, while Draft 2020-12 is the current stable standard. The two are largely compatible for everyday use cases.
The smallest valid schema is an empty object {}, which accepts any JSON value.
The most common starting point looks like this:
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"name": { "type": "string" },
"age": { "type": "integer", "minimum": 0 }
},
"required": ["name"]
}
This schema accepts any JSON object that has a name string. The age
field is optional, but if present it must be a non-negative integer.
Core Keywords Reference
The following table covers the keywords you'll use in 90% of schemas:
| Keyword | Applies to | What it does |
|---|---|---|
type | any | Constrains the JSON type: string, number, integer, boolean, array, object, null |
properties | object | Defines a map of property name → sub-schema. Properties not listed are still allowed by default. |
required | object | Array of property names that must be present. |
additionalProperties | object | false rejects keys not in properties. A schema sub-object constrains the type of extra keys. |
items | array | Schema that every element of the array must satisfy. |
minLength / maxLength | string | Character count bounds. |
pattern | string | ECMAScript regex the string must match. |
minimum / maximum | number | Numeric bounds (inclusive). Use exclusiveMinimum / exclusiveMaximum for strict bounds. |
enum | any | Value must be one of the listed items: "enum": ["red","green","blue"] |
const | any | Value must equal exactly this: "const": "active" |
$ref | any | References another schema by URI. Enables reuse and composition. |
if / then / else | any | Conditional validation: apply then when if passes, else otherwise. |
Validating JSON in JavaScript with AJV
AJV (Another JSON Schema Validator) is the dominant choice for JavaScript and Node.js. It supports Draft 07, 2019-09, and 2020-12, and it compiles schemas to optimized JavaScript functions so validation is extremely fast even at scale.
Install
npm install ajv
Basic usage
import Ajv from 'ajv';
const ajv = new Ajv();
const schema = {
type: 'object',
properties: {
name: { type: 'string', minLength: 1 },
email: { type: 'string', format: 'email' },
age: { type: 'integer', minimum: 0, maximum: 150 },
},
required: ['name', 'email'],
additionalProperties: false,
};
const validate = ajv.compile(schema);
const data = { name: 'Ada', email: 'ada@example.com', age: 36 };
if (validate(data)) {
console.log('Valid!');
} else {
console.error(validate.errors);
// [{ instancePath: '/email', message: 'must match format "email"', ... }]
}
format
keywords by default. Install npm install ajv-formats and call
addFormats(ajv) before compiling to enable "email",
"uri", "date", "uuid", and others.
TypeScript: inferring types from schemas
AJV integrates cleanly with TypeScript via type guards. Compile your schema once, then use the returned validator as a type predicate:
import Ajv, { JSONSchemaType } from 'ajv';
interface User {
name: string;
email: string;
age?: number;
}
const schema: JSONSchemaType<User> = {
type: 'object',
properties: {
name: { type: 'string' },
email: { type: 'string' },
age: { type: 'number', nullable: true },
},
required: ['name', 'email'],
};
const ajv = new Ajv();
const validate = ajv.compile<User>(schema);
function processUser(raw: unknown): User {
if (!validate(raw)) throw new Error(JSON.stringify(validate.errors));
return raw; // TypeScript now knows raw is User
}
Writing a Real-World Schema
Most real schemas validate API request bodies. Here's a complete schema for a product order:
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$defs": {
"Address": {
"type": "object",
"properties": {
"street": { "type": "string" },
"city": { "type": "string" },
"country": { "type": "string", "minLength": 2, "maxLength": 2 }
},
"required": ["street", "city", "country"],
"additionalProperties": false
}
},
"type": "object",
"properties": {
"orderId": { "type": "string", "pattern": "^ORD-[0-9]{6}$" },
"items": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"properties": {
"sku": { "type": "string" },
"quantity": { "type": "integer", "minimum": 1 }
},
"required": ["sku", "quantity"],
"additionalProperties": false
}
},
"shippingAddress": { "$ref": "#/$defs/Address" },
"billingAddress": { "$ref": "#/$defs/Address" }
},
"required": ["orderId", "items", "shippingAddress"],
"additionalProperties": false
}
Notice the $defs section. The Address schema is defined once and
referenced twice via $ref. Changing the address format in one place updates both
references automatically.
Composition: allOf, anyOf, oneOf
JSON Schema provides three keywords for combining schemas:
allOf— all listed schemas must pass (intersection / extension)anyOf— at least one schema must pass (union)oneOf— exactly one schema must pass (exclusive union)
Extending a base schema with allOf
const BaseEntity = {
type: 'object',
properties: {
id: { type: 'string' },
createdAt: { type: 'string', format: 'date-time' },
},
required: ['id', 'createdAt'],
};
const ProductSchema = {
allOf: [
BaseEntity,
{
type: 'object',
properties: {
name: { type: 'string' },
price: { type: 'number', minimum: 0 },
},
required: ['name', 'price'],
},
],
};
Discriminated unions with oneOf
{
"oneOf": [
{
"properties": {
"type": { "const": "email" },
"email": { "type": "string" }
},
"required": ["type", "email"]
},
{
"properties": {
"type": { "const": "sms" },
"phone": { "type": "string" }
},
"required": ["type", "phone"]
}
]
}
An object with type: "email" must have email. An object with
type: "sms" must have phone. Anything else is rejected.
This pattern is the JSON Schema equivalent of a TypeScript discriminated union.
Draft Versions Comparison
| Feature | Draft 04 | Draft 07 | Draft 2020-12 |
|---|---|---|---|
if / then / else | No | Yes | Yes |
$defs (was definitions) | No | definitions | $defs |
$dynamicRef | No | No | Yes |
readOnly / writeOnly | No | Yes | Yes |
contentMediaType | No | Yes | Yes |
| Library support | Declining | Widest | Growing |
Common Mistakes
1. Forgetting additionalProperties: false
Without it, extra fields are silently allowed. An API that accepts {"name":"Alice","admin":true}
when only name was defined is a security risk. Close your objects explicitly.
2. Confusing required with presence
required: ["status"] means the key must exist, but the value can be
null unless you also constrain the type. Use
"type": "string" alongside required to prevent null values.
3. Using number when you mean integer
JSON Schema's "type": "number" accepts both 1 and 1.5.
Use "type": "integer" when decimal values should be rejected. A quantity field
that accepts 2.7 units is a silent bug waiting to happen.
4. Pattern anchoring
JSON Schema regex patterns are not anchored by default. The pattern "[0-9]+"
matches any string containing at least one digit — including "abc123def".
Wrap patterns in anchors: "^[0-9]+$".
Validating JSON in the Browser
You can run AJV in the browser as well as Node.js. But for quick debugging and ad-hoc validation during development, our JSON Validator at json-format.dev supports JSON Schema Draft 07 validation directly in your browser. Paste your schema and data, and you get instant error reporting with precise field paths — no installation required.
Validate your JSON against a schema right now — no setup, no signup, 100% in-browser.
Try JSON Schema ValidatorFrequently Asked Questions
$dynamicRef, changed how $id and $anchor work, and restructured vocabulary declarations. For most projects, Draft 07 is sufficient and has broader library support.additionalProperties: false do?properties keyword, making the schema "closed." Without it, extra properties are silently allowed — a common source of schema leakage bugs.required array while keeping it in properties. The validator only enforces the type constraint when the field is present. The document is valid whether the optional field appears or not.$ref in JSON Schema?$ref references another schema by URI, enabling reuse. $ref: '#/$defs/Address' points to the Address schema defined in the $defs section of the same document. External URIs are also supported.allOf: document must be valid against ALL listed schemas. anyOf: document must be valid against AT LEAST ONE. oneOf: document must be valid against EXACTLY ONE. Use allOf for extension, anyOf for unions, and oneOf for discriminated variants.properties keyword recursively — each property value is a full sub-schema. For arrays of objects, use items to define the schema each array element must satisfy.if/then/else (added in Draft 07). If the document passes the if schema, the then schema is also enforced. If it fails if, the else schema applies. This enables cross-field rules like: if paymentType is "credit_card", then cardNumber is required.