Every developer has encountered the runtime error that happens when an API returns something unexpected: a field that should be a number is a string, a required field is missing, an array contains a null. JSON Schema is the standard way to define what your JSON data should look like — and validate it before your application processes it.
What is JSON Schema?
JSON Schema is itself a JSON document that describes the expected structure of another JSON document. It specifies:
- Which fields are required vs optional
- What type each field should be
- Constraints on values (min/max, patterns, enums)
- Nested object and array shapes
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"required": ["id", "name", "email"],
"properties": {
"id": {
"type": "integer",
"minimum": 1
},
"name": {
"type": "string",
"minLength": 1,
"maxLength": 100
},
"email": {
"type": "string",
"format": "email"
},
"role": {
"type": "string",
"enum": ["admin", "user", "viewer"]
},
"tags": {
"type": "array",
"items": {
"type": "string"
},
"uniqueItems": true
},
"createdAt": {
"type": "integer",
"description": "Unix timestamp"
}
},
"additionalProperties": false
}
Core types
JSON Schema supports these primitive types:
-
"string"— any string value -
"number"— any number (integer or float) -
"integer"— integer only -
"boolean"—trueorfalse -
"null"—nullonly -
"array"— JSON array -
"object"— JSON object -
"type": ["string", "null"]— multiple types (nullable)
String constraints
{
"type": "string",
"minLength": 1,
"maxLength": 255,
"pattern": "^[a-z0-9-]+$", // regex
"format": "email" // built-in format validators
}
Built-in formats: "email", "uri", "date", "time", "date-time", "ipv4", "ipv6", "uuid". Note: format validation is optional in the spec — check your library's documentation.
Number constraints
{
"type": "number",
"minimum": 0,
"maximum": 100,
"exclusiveMinimum": true, // 0 is not valid, only > 0
"multipleOf": 0.5 // must be a multiple of 0.5
}
Object constraints
{
"type": "object",
"required": ["name", "email"],
"properties": {
"name": { "type": "string" },
"email": { "type": "string", "format": "email" },
"nickname": { "type": "string" }
},
"additionalProperties": false, // reject unknown keys
"minProperties": 2,
"maxProperties": 10
}
additionalProperties: false is strict — only keys listed in properties are allowed. Useful for catching typos in field names during development.
Array constraints
{
"type": "array",
"items": { "type": "string" }, // all items must be strings
"minItems": 1,
"maxItems": 100,
"uniqueItems": true
}
For heterogeneous arrays where each position has a known type (a "tuple"):
{
"type": "array",
"prefixItems": [
{ "type": "string" }, // position 0: string
{ "type": "number" }, // position 1: number
{ "type": "boolean" } // position 2: boolean
],
"items": false // no additional items allowed
}
Conditional schemas
{
"type": "object",
"properties": {
"type": { "type": "string", "enum": ["user", "admin"] },
"adminLevel": { "type": "integer" }
},
"if": {
"properties": { "type": { "const": "admin" } }
},
"then": {
"required": ["adminLevel"]
}
}
adminLevel is only required when type is "admin".
Composition with allOf, anyOf, oneOf
{
"allOf": [
{ "$ref": "#/$defs/BaseUser" },
{ "required": ["adminLevel"] }
]
}
{
"anyOf": [
{ "type": "string" },
{ "type": "null" }
]
}
allOf — must match all schemas. anyOf — must match at least one. oneOf — must match exactly one.
Reusable definitions
{
"$defs": {
"Address": {
"type": "object",
"required": ["street", "city"],
"properties": {
"street": { "type": "string" },
"city": { "type": "string" },
"country": { "type": "string", "default": "US" }
}
}
},
"type": "object",
"properties": {
"billingAddress": { "$ref": "#/$defs/Address" },
"shippingAddress": { "$ref": "#/$defs/Address" }
}
}
$ref references any schema by its JSON Pointer path.
Validation in JavaScript (Ajv)
Ajv is the most widely used JSON Schema validator for JavaScript:
import Ajv from 'ajv';
import addFormats from 'ajv-formats';
const ajv = new Ajv();
addFormats(ajv);
const schema = {
type: 'object',
required: ['name', 'email'],
properties: {
name: { type: 'string', minLength: 1 },
email: { type: 'string', format: 'email' },
},
};
const validate = ajv.compile(schema);
const data = { name: 'Alice', email: 'alice@example.com' };
if (!validate(data)) {
console.error(validate.errors);
// [{ instancePath: '/email', message: 'must match format "email"' }]
} else {
console.log('Valid!');
}
Validation in Python (jsonschema)
from jsonschema import validate, ValidationError
schema = {
"type": "object",
"required": ["name", "email"],
"properties": {
"name": {"type": "string", "minLength": 1},
"email": {"type": "string", "format": "email"},
}
}
data = {"name": "Alice", "email": "not-an-email"}
try:
validate(instance=data, schema=schema)
except ValidationError as e:
print(e.message) # 'not-an-email' is not a 'email'
Where to use schema validation
Incoming API requests — validate request bodies before passing to business logic. Return 400 with a structured error message if validation fails.
Outgoing API responses — validate your own API output in tests to catch field-dropping regressions.
Configuration files — validate JSON/YAML config at startup before the application runs.
Message queue payloads — validate events before processing to avoid corrupt state.
Formatting and inspecting JSON
When debugging schemas or API responses, a formatter helps spot structural issues at a glance. The JSON Formatter formats, validates, and syntax-highlights JSON in the browser with no upload required — useful for inspecting API responses from your browser's Network tab.
JSON Schema is one of those tools that pays back every hour you put into it. A schema you write once catches an entire class of bugs across development, testing, and production — before they hit your error handling or, worse, your database.
Top comments (0)