JSON Schema Validation Guide — Draft 7, 2020-12, AJV & OpenAPI Explained
JSON Schema is the standard way to describe and validate the structure of JSON data. A schema defines which fields are required, what types values must be, what patterns strings must match, and what ranges numbers must fall within. This guide covers the JSON Schema keywords you actually use, the differences between drafts, how to use AJV in Node.js, and how to validate JSON against a schema online without writing code.
5 drafts
Draft 4, 6, 7, 2019-09, 2020-12 — each adds new keywords and fixes
AJV
Most popular JSON Schema validator — used in Express, Fastify, OpenAPI tooling
1 paste
Validate any JSON against any schema online — no code required
What Is JSON Schema and Why Use It?
JSON Schema is a vocabulary for describing the shape of JSON data. A JSON Schema document (itself written in JSON) specifies the rules that a JSON value must follow. Validators check JSON data against the schema and report every violation.
API request validation
Reject invalid API payloads before your business logic runs. Return a detailed 400 error showing exactly which fields are wrong — better UX than a cryptic 500 from a downstream null reference.
Config file validation
Validate CI configs, Docker Compose overrides, and app config files against a schema. Catch missing required fields and wrong value types before deploying.
Data pipeline validation
Validate each record in a data pipeline before ingestion. Schema validation at the entry point prevents corrupt data from propagating to databases and downstream consumers.
OpenAPI / Swagger schemas
OpenAPI 3.0 uses JSON Schema Draft 7 for request/response body schemas. OpenAPI 3.1 uses Draft 2020-12. API code generators use schemas to produce typed clients automatically.
Core JSON Schema Keywords You Use Every Day
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "User",
"type": "object",
"required": ["id", "name", "email", "role"],
"properties": {
"id": {
"type": "integer",
"minimum": 1
},
"name": {
"type": "string",
"minLength": 2,
"maxLength": 100,
"pattern": "^[A-Za-z ]+$"
},
"email": {
"type": "string",
"format": "email"
},
"role": {
"type": "string",
"enum": ["admin", "editor", "viewer", "guest"]
},
"age": {
"type": "integer",
"minimum": 0,
"maximum": 150
},
"tags": {
"type": "array",
"items": { "type": "string" },
"minItems": 1,
"maxItems": 10,
"uniqueItems": true
},
"score": {
"type": "number",
"multipleOf": 0.5
},
"metadata": {
"type": ["object", "null"]
}
},
"additionalProperties": false
}type — value type constraint
Valid types: "string", "number", "integer", "boolean", "array", "object", "null". Use an array to allow multiple types: "type": ["string", "null"] for nullable strings.
required — mandatory fields
An array of property names that must be present. Validation fails if any required property is missing from the object. Fields in properties but not required are optional.
enum — fixed set of allowed values
"enum": ["admin", "editor", "viewer"] — value must exactly match one of the listed values. Works for any type. For a single allowed value, use "const": "active" instead.
format — semantic string validation
Built-in formats (with ajv-formats): "email", "uri", "url", "date", "date-time", "time", "uuid", "ipv4", "ipv6", "hostname". Not validated by default — requires ajv-formats plugin.
JSON Schema Drafts — Which One to Use
Draft 7 — most widely supported
Last draft before the 2019 redesign. Supported by AJV 6, OpenAPI 3.0, and most existing tooling. $schema: "http://json-schema.org/draft-07/schema#". Use for OpenAPI 3.0 and legacy projects.
Draft 2020-12 — current standard
Current release. Uses $defs (not definitions), adds unevaluatedProperties and prefixItems. Required by OpenAPI 3.1. $schema: "https://json-schema.org/draft/2020-12/schema". Use for new projects.
Draft 2019-09
Intermediate between 7 and 2020-12. Renamed definitions to $defs, changed $ref. Mostly superseded by 2020-12. Supported by AJV 8 with draft2019 option.
Draft 4 and 6 — legacy
Draft 4 still used in MongoDB and some enterprise tooling. Draft 6 added const and contains. Both superseded by Draft 7. Supported in AJV 8 via legacy meta-schemas.
// ---- Draft 7 ----
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"Address": {
"type": "object",
"required": ["city", "country"],
"properties": {
"city": { "type": "string" },
"country": { "type": "string" }
}
}
},
"type": "object",
"properties": {
"address": { "$ref": "#/definitions/Address" }
}
}
// ---- Draft 2020-12 ----
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$defs": {
"Address": {
"type": "object",
"required": ["city", "country"],
"properties": {
"city": { "type": "string" },
"country": { "type": "string" }
}
}
},
"type": "object",
"properties": {
"address": { "$ref": "#/$defs/Address" }
}
}
// Key differences:
// definitions → $defs
// $ref works correctly alongside other keywords in 2020-12
// unevaluatedProperties available in 2020-12Schema Composition — allOf, anyOf, oneOf, not
// allOf — must satisfy ALL sub-schemas (AND)
// Use for extending a base schema:
{
"allOf": [
{ "$ref": "#/$defs/BaseUser" },
{
"required": ["adminSince"],
"properties": { "adminSince": { "type": "string", "format": "date" } }
}
]
}
// anyOf — must satisfy AT LEAST ONE sub-schema (OR)
// Use for union types — accepts string or number ID:
{
"anyOf": [
{ "type": "string", "pattern": "^usr_" },
{ "type": "integer", "minimum": 1 }
]
}
// oneOf — must satisfy EXACTLY ONE sub-schema (XOR)
// Use for mutually exclusive payment methods:
{
"type": "object",
"oneOf": [
{ "required": ["bankAccount"], "not": { "required": ["creditCard"] } },
{ "required": ["creditCard"], "not": { "required": ["bankAccount"] } }
]
}
// if / then / else — conditional validation (Draft 7+)
// Require zipCode for US, postcode for other countries:
{
"if": { "properties": { "country": { "const": "US" } }, "required": ["country"] },
"then": { "required": ["zipCode"] },
"else": { "required": ["postcode"] }
}AJV — JSON Schema Validation in Node.js
// npm install ajv ajv-formats
import Ajv from 'ajv'; // AJV 8 — Draft 7 by default
import Ajv2020 from 'ajv/dist/2020'; // Draft 2020-12
import addFormats from 'ajv-formats';
// ---- Draft 7 setup ----
const ajv = new Ajv({
allErrors: true, // report ALL errors, not just the first
strict: true, // error on unknown keywords
coerceTypes: false, // never coerce "42" string to 42 integer
});
addFormats(ajv); // enables email, uri, date-time, uuid, etc.
// ---- Draft 2020-12 setup ----
const ajv2020 = new Ajv2020({ allErrors: true });
addFormats(ajv2020);
// Compile schema ONCE at startup — expensive operation
const userSchema = {
type: 'object',
required: ['id', 'name', 'email'],
properties: {
id: { type: 'integer', minimum: 1 },
name: { type: 'string', minLength: 2 },
email: { type: 'string', format: 'email' },
role: { type: 'string', enum: ['admin', 'editor', 'viewer'] },
},
additionalProperties: false,
};
const validateUser = ajv.compile(userSchema);
// Use the compiled validator — fast, reusable
function validateAndCreate(data) {
const valid = validateUser(data);
if (!valid) {
const errors = validateUser.errors.map(e => ({
path: e.instancePath || '(root)',
message: e.message,
}));
throw new ValidationError('Invalid user data', errors);
}
return createUser(data);
}Compile once — validate many times
ajv.compile(schema) compiles the schema into an optimized JS function. Call it once at application startup, store the compiled validator. Calling the compiled validator (validate(data)) is ~100x faster than compiling on each request. Never call compile() inside a request handler.additionalProperties vs unevaluatedProperties — getting it wrong breaks composition
additionalProperties ignores $ref properties — false positives
// additionalProperties: false breaks when using allOf/anyOf/$ref
{
"allOf": [
{ "$ref": "#/$defs/BaseUser" },
{
"additionalProperties": false, // only sees local properties!
"properties": { "adminSince": { "type": "string" } }
}
]
}
// Result: id, name, email from BaseUser are flagged as "additional properties"
// because additionalProperties doesn't look at $ref propertiesunevaluatedProperties sees all evaluated properties — correct
// unevaluatedProperties: false works correctly with composition (Draft 2019-09+)
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"allOf": [
{ "$ref": "#/$defs/BaseUser" }
],
"properties": { "adminSince": { "type": "string" } },
"unevaluatedProperties": false // sees ALL properties including from $ref
}
// Result: id, name, email (from BaseUser) + adminSince are all allowed
// Any other property causes a validation errorValidate JSON Against a Schema Online
Paste your JSON data
Go to unblockdevs.com/json-validator. Paste the JSON you want to validate in the data panel. Syntax errors appear immediately with line and column numbers.
Paste your JSON Schema
Add your schema in the schema panel. You can use schemas from your codebase, from API documentation (OpenAPI), or write a new schema to test validation rules.
Select the schema draft
Choose Draft 7 for OpenAPI 3.0 and most existing schemas. Choose Draft 2020-12 for new projects and OpenAPI 3.1. The validator auto-detects the draft from the $schema field.
Review validation errors
Each error shows the JSON path (e.g., /user/email), the constraint that failed (format, minLength, required), and the actual value. Fix the JSON or refine the schema based on the report.