Home → JSON Schema Guide
JSON Schema: Complete Guide to Validating JSON Data (2026)
JSON Schema is a powerful standard that lets you describe and validate the structure of JSON data. Whether you're building a REST API, validating configuration files, or enforcing data contracts between microservices, JSON Schema gives you a declarative, language-agnostic way to specify exactly what your JSON must look like — and reject anything that doesn't conform.
This guide covers everything from the basics of schema structure to advanced topics like $ref references, conditional validation with if/then/else, and practical validation in both JavaScript (using Ajv) and Python (using jsonschema). By the end, you'll be able to write production-quality schemas for real-world APIs.
Validate JSON against a schema right now
Use the free JSON Schema Validator — paste your schema and data to see errors instantly.
Open Schema Validator →1. What is JSON Schema?
JSON Schema is a vocabulary that allows you to annotate and validate JSON documents. It is itself written in JSON, which means you can store schemas in files, send them over APIs, and process them with standard JSON tools. The official home of the specification is json-schema.org, where the IETF Internet-Draft documents live.
The specification has evolved through several draft versions. The most commonly used today are:
- Draft-04 — the first widely adopted version, introduced
$refand the core keywords. - Draft-06 — added
contains,propertyNames,const, andexamples. - Draft-07 — the most popular stable version; added
if/then/else,readOnly,writeOnly. - 2019-09 (Draft-08) — modularized the spec into vocabularies, introduced
$anchor,$recursiveRef. - 2020-12 — the current official version; replaced
$recursiveRefwith$dynamicRef, changed howitemsandadditionalItemswork.
When writing schemas for new projects, specify the draft with the $schema keyword so validators know which rules to apply. Most validator libraries still default to draft-07 behavior, which remains the sweet spot of stability and feature richness for 2026 projects.
Why JSON Schema matters: Without schema validation, your API accepts any shape of data and you discover problems at runtime — often in production. With JSON Schema, you catch type mismatches, missing fields, and malformed values at the boundary of your system, before they corrupt your database or crash your service.
2. Basic Schema Structure
Every JSON Schema is a JSON object. The four most fundamental keywords are $schema, type, properties, and required. Here is a minimal schema that validates a user object:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "User",
"description": "A registered user in the system",
"type": "object",
"properties": {
"id": {
"type": "integer",
"description": "Unique user identifier"
},
"username": {
"type": "string",
"minLength": 3,
"maxLength": 30
},
"email": {
"type": "string",
"format": "email"
},
"active": {
"type": "boolean"
}
},
"required": ["id", "username", "email"]
}
This schema validates the following JSON successfully:
{
"id": 42,
"username": "alice",
"email": "alice@example.com",
"active": true
}
The $schema keyword is a URI that identifies the schema dialect. The title and description keywords are purely for documentation — validators ignore them. The real work is done by type, properties, and required.
3. Primitive Type Validation
JSON Schema supports six primitive types that map directly to JSON's native data types:
| Schema type | JSON example | Notes |
|---|---|---|
"string" | "hello" | Any UTF-8 string |
"number" | 3.14, 42 | Integers and floats |
"integer" | 42 | Only whole numbers |
"boolean" | true, false | Strict booleans only |
"null" | null | JSON null literal |
"array" | [1, 2, 3] | Ordered list |
"object" | {"key": "val"} | Key-value pairs |
You can also accept multiple types by using an array: "type": ["string", "null"]. This is the correct way to express a nullable field — it means the value can be a string or null, but not a number or boolean.
If you omit the type keyword entirely, the schema accepts any JSON value. An empty object {} is a valid schema that passes everything — useful as a placeholder but be careful not to leave it accidentally in production schemas.
4. String Validation
Strings have several keywords that let you constrain their length, content, and format:
Length constraints
{
"type": "string",
"minLength": 1,
"maxLength": 255
}
minLength and maxLength count Unicode code points, not bytes. The string "café" has 4 code points, even though it may be 5 bytes in UTF-8.
Pattern (regular expressions)
{
"type": "string",
"pattern": "^[a-z][a-z0-9_]{2,29}$"
}
The pattern keyword takes an ECMA-262 regular expression. The regex is tested as a search (not an anchored match), so include ^ and $ anchors if you want to match the entire string. The example above validates lowercase usernames of 3–30 characters starting with a letter.
Format
The format keyword provides semantic validation for common string types:
"email"— an email address"uri"— a fully qualified URI"date"— a date in YYYY-MM-DD format"date-time"— a date-time in ISO 8601 format"time"— a time in HH:MM:SS format"uuid"— a UUID string"ipv4"and"ipv6"— IP addresses"hostname"— a valid DNS hostname
Important: Format validation is optional in JSON Schema — validators are not required to enforce it unless explicitly configured. In Ajv, install the ajv-formats package and call addFormats(ajv). In Python's jsonschema, pass format_checker=jsonschema.FormatChecker() to the validate call.
5. Number Validation
Both "number" and "integer" types support range and divisibility constraints:
{
"type": "number",
"minimum": 0,
"maximum": 100,
"exclusiveMaximum": 100,
"multipleOf": 0.5
}
The keywords work as follows:
minimum— the value must be greater than or equal to this number (inclusive lower bound).maximum— the value must be less than or equal to this number (inclusive upper bound).exclusiveMinimum— the value must be strictly greater than this number (not equal).exclusiveMaximum— the value must be strictly less than this number (not equal).multipleOf— the value must be a multiple of this number. Useful for step values, currency amounts, or integer grid constraints.
Draft-04 note: In draft-04, exclusiveMinimum and exclusiveMaximum were booleans that modified the corresponding minimum/maximum keywords. From draft-06 onward, they became standalone numeric keywords. This is a common source of confusion when mixing libraries and schema versions.
6. Object Validation
Objects are the most common JSON structure in real APIs. JSON Schema gives you precise control over which properties are allowed, required, or prohibited:
properties and required
{
"type": "object",
"properties": {
"firstName": { "type": "string" },
"lastName": { "type": "string" },
"age": { "type": "integer", "minimum": 0 }
},
"required": ["firstName", "lastName"]
}
The properties keyword defines schemas for known properties. It does not make those properties required — that is the job of the required keyword, which takes an array of property names that must be present.
additionalProperties
{
"type": "object",
"properties": {
"x": { "type": "number" },
"y": { "type": "number" }
},
"additionalProperties": false
}
Setting additionalProperties to false rejects any property not listed in properties. This is the "closed" schema pattern. You can also set it to a schema — "additionalProperties": {"type": "string"} — to allow extra properties as long as they match the given type.
patternProperties
{
"type": "object",
"patternProperties": {
"^S_": { "type": "string" },
"^N_": { "type": "number" }
}
}
patternProperties applies schemas to all properties whose names match a regex. The example above requires all S_-prefixed properties to be strings and all N_-prefixed properties to be numbers.
minProperties and maxProperties
Use minProperties and maxProperties to constrain how many properties an object may contain. This is useful for generic key-value maps where you want to limit the number of entries.
7. Array Validation
Arrays are validated with items, which applies a schema to every element in the array, plus several keywords for length and uniqueness:
{
"type": "array",
"items": {
"type": "string",
"minLength": 1
},
"minItems": 1,
"maxItems": 10,
"uniqueItems": true
}
This schema validates an array of 1 to 10 non-empty strings, with no duplicates — a tag list, for example.
Tuple validation (prefixItems)
In JSON Schema 2020-12, prefixItems replaces the array form of items for tuple validation:
{
"type": "array",
"prefixItems": [
{ "type": "number" },
{ "type": "string" },
{ "type": "boolean" }
],
"items": false
}
This validates a fixed-length tuple [42, "hello", true] where the first element is a number, the second is a string, and the third is a boolean. Setting items: false disallows any additional elements beyond the three defined positions.
contains
The contains keyword validates that at least one element in the array matches a given schema. Add minContains and maxContains to control how many matching elements are required:
{
"type": "array",
"contains": { "type": "number", "minimum": 100 },
"minContains": 1,
"maxContains": 3
}
8. Combining Schemas
JSON Schema provides four composition keywords that let you build complex validation logic from simpler schemas:
allOf — all schemas must pass
{
"allOf": [
{ "type": "string" },
{ "minLength": 5 },
{ "pattern": "^[A-Z]" }
]
}
The data must be valid against every schema in the array. This is the equivalent of a logical AND. Use it to layer additional constraints onto a base schema.
anyOf — at least one schema must pass
{
"anyOf": [
{ "type": "string" },
{ "type": "number" }
]
}
The data must be valid against at least one schema. This is a logical OR. It is the correct way to express union types in JSON Schema.
oneOf — exactly one schema must pass
{
"oneOf": [
{ "properties": { "type": { "const": "circle" } }, "required": ["type", "radius"] },
{ "properties": { "type": { "const": "rect" } }, "required": ["type", "width", "height"] }
]
}
oneOf validates that exactly one sub-schema matches. It is more expensive to evaluate than anyOf because it must check all schemas even after one passes. Use it for true discriminated unions where the sub-schemas are mutually exclusive.
not — schema must not pass
{
"not": { "type": "null" }
}
The data must fail the given schema. This example rejects null values. not is useful for excluding specific values while allowing everything else.
9. $ref and $defs for Reusable Schemas
As schemas grow, you'll find yourself repeating the same sub-schemas in multiple places. The $defs keyword (called definitions in older drafts) is the standard place to put reusable schema fragments, and $ref is used to reference them:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$defs": {
"address": {
"type": "object",
"properties": {
"street": { "type": "string" },
"city": { "type": "string" },
"country": { "type": "string", "minLength": 2, "maxLength": 2 }
},
"required": ["street", "city", "country"]
},
"positiveInteger": {
"type": "integer",
"minimum": 1
}
},
"type": "object",
"properties": {
"id": { "$ref": "#/$defs/positiveInteger" },
"billingAddress": { "$ref": "#/$defs/address" },
"shippingAddress":{ "$ref": "#/$defs/address" }
},
"required": ["id", "billingAddress"]
}
The $ref value is a JSON Pointer. The # refers to the root of the current schema document. /$defs/address is the path to the address definition. This DRY (Don't Repeat Yourself) approach means you change the address schema in one place and all references automatically pick up the change.
You can also reference external schemas by providing a full URI: "$ref": "https://example.com/schemas/address.json". Most validators support loading external $refs, either automatically or through a registered schema store.
10. Conditional Validation
One of the most useful features added in draft-07 is the if/then/else construct. It lets you apply different validation rules depending on whether the data matches a condition:
{
"type": "object",
"properties": {
"paymentMethod": { "type": "string", "enum": ["card", "bank", "paypal"] },
"cardNumber": { "type": "string", "pattern": "^[0-9]{16}$" },
"bankAccount": { "type": "string" },
"paypalEmail": { "type": "string", "format": "email" }
},
"required": ["paymentMethod"],
"if": {
"properties": { "paymentMethod": { "const": "card" } }
},
"then": {
"required": ["cardNumber"]
},
"else": {
"if": {
"properties": { "paymentMethod": { "const": "bank" } }
},
"then": {
"required": ["bankAccount"]
},
"else": {
"required": ["paypalEmail"]
}
}
}
If the if schema validates successfully, the then schema is applied. If it fails, the else schema is applied. The else branch can itself contain another if/then/else for chained conditions.
This pattern is ideal for forms and API requests where different fields become required depending on a discriminator field like paymentMethod, subscriptionType, or role.
11. Validating JSON Schema in JavaScript with Ajv
Ajv (Another JSON Validator) is the fastest and most feature-complete JSON Schema validator for JavaScript and Node.js. It pre-compiles schemas to JavaScript functions for maximum performance.
Installation
npm install ajv ajv-formats
Basic usage
import Ajv from "ajv";
import addFormats from "ajv-formats";
const ajv = new Ajv({ allErrors: true });
addFormats(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: "Alice", email: "alice@example.com", age: 28 };
if (validate(data)) {
console.log("Valid!");
} else {
console.error("Validation errors:", validate.errors);
}
Reading Ajv error messages
When validation fails, validate.errors is an array of error objects. Each object has these key fields:
instancePath— the JSON Pointer path to the failing value (e.g.,/email).schemaPath— the path in the schema that failed (e.g.,#/properties/email/format).keyword— the keyword that failed (e.g.,"format","required","type").message— a human-readable error message.params— additional context (e.g., the missing property name forrequirederrors).
// Example error output for missing required field:
[
{
instancePath: "",
schemaPath: "#/required",
keyword: "required",
params: { missingProperty: "email" },
message: "must have required property 'email'"
}
]
Ajv with JSON Schema 2020-12
import Ajv2020 from "ajv/dist/2020";
import addFormats from "ajv-formats";
const ajv = new Ajv2020({ allErrors: true });
addFormats(ajv);
12. Validating in Python with jsonschema
The jsonschema library is the standard JSON Schema validator for Python. It supports drafts 4, 6, 7, 2019-09, and 2020-12.
Installation
pip install jsonschema[format-nongpl]
The [format-nongpl] extra installs format validators (for email, uri, etc.) that use non-GPL dependencies.
Basic validation
import jsonschema
from jsonschema import validate, ValidationError, FormatChecker
schema = {
"type": "object",
"properties": {
"name": {"type": "string", "minLength": 1},
"email": {"type": "string", "format": "email"},
"age": {"type": "integer", "minimum": 0}
},
"required": ["name", "email"]
}
data = {"name": "Alice", "email": "alice@example.com", "age": 28}
try:
validate(instance=data, schema=schema, format_checker=FormatChecker())
print("Valid!")
except ValidationError as e:
print(f"Validation error: {e.message}")
print(f"Path: {list(e.absolute_path)}")
print(f"Schema path: {list(e.absolute_schema_path)}")
Collecting all errors
By default, validate() raises on the first error. To collect all errors, use a Validator object directly:
from jsonschema import Draft202012Validator
validator = Draft202012Validator(schema, format_checker=FormatChecker())
errors = list(validator.iter_errors(data))
for error in sorted(errors, key=lambda e: e.path):
print(f" - {'.'.join(str(p) for p in error.path)}: {error.message}")
13. Common Schema Mistakes
Wrong type names
JSON Schema type names are lowercase strings. "type": "String", "type": "Integer", and "type": "Boolean" are all invalid — the correct names are "string", "integer", and "boolean".
Forgetting required
Listing a property in properties does not make it mandatory. A schema with "properties": {"name": {"type": "string"}} happily validates an empty object {}. You must add "required": ["name"] to enforce that the property is present.
The additionalProperties trap
Setting "additionalProperties": false is strict — it blocks any property not listed in properties. This causes unexpected failures when you use allOf to compose schemas, because each sub-schema only knows about its own properties. The safe pattern for composition is to avoid additionalProperties: false at the allOf level, or use unevaluatedProperties: false (2019-09+) which is composition-aware.
Using definitions instead of $defs
The key definitions was a convention in draft-04 through draft-07, but it was never an official keyword — validators just happened to support it as a base URI for $ref. From 2019-09 onward, $defs is the official keyword. Use $defs for new schemas and ensure your validator supports the draft version you're using.
Assuming number validates integers
In JSON Schema, "type": "integer" is a sub-type of "number". An integer value like 5 passes both "type": "number" and "type": "integer". But 5.0 passes "type": "number" and fails "type": "integer". If your API receives floats that happen to be whole numbers, decide upfront which type you actually need.
14. JSON Schema vs TypeScript Types
Both JSON Schema and TypeScript describe the shape of data, but they serve different purposes and work at different points in the development pipeline:
| Aspect | JSON Schema | TypeScript Types |
|---|---|---|
| When it runs | Runtime validation | Compile-time type checking |
| Language | Language-agnostic JSON | TypeScript only |
| Validates external data | Yes | No (types are erased at runtime) |
| Regex constraints | Yes (pattern) | No (template literal types only) |
| Range constraints | Yes (minimum, maximum) | No |
| Generates documentation | Yes | With tools (TSDoc) |
| Stored in version control | JSON file | .ts file |
TypeScript cannot validate data coming from a network request, a database, or a file at runtime — types are completely erased during compilation. JSON Schema is what you need to validate data at your API's entry point. The two are complementary, not competing.
Generating one from the other
Several tools bridge the gap between JSON Schema and TypeScript:
- json-schema-to-typescript (
npm install -g json-schema-to-typescript) — generates TypeScript interfaces from JSON Schema files. Excellent for keeping your runtime schema and compile-time types in sync. - typescript-json-schema — generates a JSON Schema from TypeScript interfaces. Useful if you already have TypeScript types and want to derive a runtime validator.
- Zod — a TypeScript-first schema library that can export to JSON Schema via
zod-to-json-schema. Gives you both compile-time safety and runtime validation from a single source of truth. - Ajv's type provider — Ajv can narrow TypeScript types after successful validation so your validated data is properly typed in subsequent code.
The most maintainable approach for large TypeScript projects is to define schemas in Zod or write JSON Schema files and generate TypeScript interfaces from them — keeping the schema as the source of truth and the TypeScript types as a derived artifact.
15. Frequently Asked Questions
npm install ajv ajv-formats, then compile your schema with const validate = ajv.compile(schema) and call validate(data). If it returns false, check validate.errors for the list of validation failures. Each error includes the path to the failing value, the keyword that failed, and a human-readable message.format keyword with values like "email", "uri", "date", or "ipv4". However, format validation is optional by default in most validators. In Ajv, enable it by installing the ajv-formats package and calling addFormats(ajv). In Python's jsonschema library, pass format_checker=jsonschema.FormatChecker() to the validate function.