Anatomy of a Schema
Every JSON Schema document is itself a JSON object. The $schema keyword declares the dialect version. Everything else describes constraints on the instance being validated:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://api.example.com/schemas/user",
"title": "User",
"description": "A registered user account",
"type": "object",
"properties": {
"id": { "type": "string" },
"name": { "type": "string", "minLength": 1, "maxLength": 100 },
"email": { "type": "string", "format": "email" },
"age": { "type": "integer", "minimum": 0, "maximum": 150 },
"role": { "type": "string", "enum": ["admin", "editor", "viewer"] }
},
"required": ["id", "name", "email"],
"additionalProperties": false
}The Six Primitive Types
| Type keyword | JSON values it matches | Common keywords |
|---|---|---|
| string | Any JSON string | minLength, maxLength, pattern, format |
| number | Any JSON number (int or float) | minimum, maximum, exclusiveMinimum, multipleOf |
| integer | Numbers with no fractional part | Same as number |
| boolean | true or false only | — |
| null | JSON null only | — |
| object | JSON objects {} | properties, required, additionalProperties, minProperties |
| array | JSON arrays [] | items, prefixItems, minItems, maxItems, uniqueItems |
Schema Composition Keywords
Four Boolean logic operators let you combine schemas:
// allOf: must match ALL schemas (intersection)
"allOf": [
{ "type": "object" },
{ "required": ["id"] }
]
// anyOf: must match AT LEAST ONE schema (union)
"anyOf": [
{ "type": "string", "format": "date" },
{ "type": "null" }
]
// oneOf: must match EXACTLY ONE schema (XOR)
"oneOf": [
{ "type": "string" },
{ "type": "integer" }
]
// not: must NOT match the schema
"not": { "type": "null" } // disallow null$ref and Schema Reuse
Complex schemas grow quickly. $ref lets you reference reusable sub-schemas to avoid repetition:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$defs": {
"Address": {
"type": "object",
"properties": {
"street": { "type": "string" },
"city": { "type": "string" },
"pin": { "type": "string", "pattern": "^[0-9]{6}$" }
},
"required": ["street", "city", "pin"]
}
},
"type": "object",
"properties": {
"billingAddress": { "$ref": "#/$defs/Address" },
"shippingAddress": { "$ref": "#/$defs/Address" }
}
}Conditional Validation with if/then/else
JSON Schema 2019-09+ supports conditional logic without workarounds:
{
"type": "object",
"properties": {
"paymentMethod": { "type": "string" },
"cardNumber": { "type": "string" },
"upiId": { "type": "string" }
},
"if": { "properties": { "paymentMethod": { "const": "card" } } },
"then": { "required": ["cardNumber"] },
"else": { "required": ["upiId"] }
}Format Keywords
The format keyword annotates the expected string format. By default it is advisory (validation libraries may or may not enforce it). Enable strict format validation explicitly in your validator configuration.
| format value | Description |
|---|---|
| date-time | ISO 8601 datetime with timezone (e.g., "2026-06-07T10:30:00Z") |
| date | Full date: "2026-06-07" |
| time | Full time: "10:30:00Z" |
| RFC 5321 email address | |
| uri | RFC 3986 URI |
| uuid | RFC 4122 UUID (e.g., "550e8400-e29b-41d4-a716-446655440000") |
| ipv4 | IPv4 address in dotted-decimal notation |
| ipv6 | IPv6 address |
| hostname | RFC 1123 hostname |
Popular Validators by Language
- JavaScript/TypeScript:
ajv(most widely used, draft 2020-12 support) - Python:
jsonschema,fastjsonschema - Java:
networknt/json-schema-validator - Go:
santhosh-tekuri/jsonschema - Rust:
jsonschemacrate - PHP:
opis/json-schema
Setting "additionalProperties": false is the most impactful single addition to most schemas. It rejects any property not explicitly listed in properties, preventing typos and undocumented fields from silently passing through. Always set it for API request body schemas.