JSON Schema is a declarative language for describing the shape of JSON data. Instead of hand-writing validation logic in every service, you write one schema document that says what fields exist, which are required, what types they hold, and what values are allowed. Any conforming validator can then check data against it and produce consistent, machine-readable errors.
This guide covers the current dialect, draft 2020-12, from the core keywords to the boolean combinators (oneOf, anyOf, allOf, not) and the $ref/$defs reuse mechanism. It ends with a worked schema and runnable validation examples in JavaScript and Python, plus how to go the other way and generate a schema from existing data.
What JSON Schema Is and Why It Exists
A JSON Schema is itself a JSON document. It uses keywords to constrain the data it describes: a value must be a string, an object must have a name property, an array's items must be numbers between 0 and 100, and so on. When a validator runs a schema against an instance (the data being checked), it returns either success or a list of errors pointing at exactly which constraints failed.
The payoff is a single source of truth for validation. One schema can guard an HTTP API's request body, drive form validation in the browser, generate documentation, produce fake data for tests, and even scaffold TypeScript types. You write the rules once and reuse them everywhere, instead of duplicating brittle if checks across a codebase.
- API contracts — reject malformed request and response payloads before they reach business logic.
- Configuration files — validate a service config at startup and fail fast with clear messages.
- Data pipelines — gate records entering a store or queue so bad data never lands.
- Tooling — editors like VS Code use JSON Schema to power autocomplete and inline errors in JSON and YAML files.
Draft 2020-12 and the $schema Keyword
JSON Schema is versioned into dialects called drafts. The current, widely supported one is draft 2020-12. Older documents often reference draft-07; the core keywords are nearly identical, but 2020-12 changed how arrays and references work in a few important ways. Always declare which dialect you use with the $schema keyword so validators interpret keywords correctly.
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://example.com/schemas/user.json",
"title": "User",
"type": "object"
}$id assigns the schema a base URI. It is what other schemas point at with $ref, and it anchors relative references resolved inside the document. title and description are annotations — they do not constrain data, but tools surface them in docs and error messages, so they are worth writing.
The Core Keywords
Most real schemas are built from a small set of keywords, grouped by the type of value they apply to.
Type and object structure
| Keyword | Applies to | What it does |
|---|---|---|
| type | any | Restricts the JSON type: string, number, integer, boolean, object, array, or null. Can be an array of types. |
| properties | object | Maps property names to subschemas that their values must satisfy. |
| required | object | Lists property names that must be present (independent of properties). |
| additionalProperties | object | Controls properties not named in properties: false forbids them, or a schema constrains them. |
| items | array | Schema every array element must match. |
| enum | any | The value must equal one of a fixed list of allowed values. |
| const | any | The value must equal exactly this one value. |
String, number, and array constraints
| Keyword | Applies to | What it does |
|---|---|---|
| minLength / maxLength | string | Bounds on string length in characters. |
| pattern | string | A regular expression the string must match (unanchored by default). |
| format | string | Semantic hint like email, date-time, uri, uuid — enforced only if the validator opts in. |
| minimum / maximum | number | Inclusive numeric bounds. Use exclusiveMinimum / exclusiveMaximum for strict bounds. |
| multipleOf | number | The number must be an exact multiple of this value. |
| minItems / maxItems | array | Bounds on array length. |
| uniqueItems | array | When true, all array elements must be distinct. |
Combining Schemas: oneOf, anyOf, allOf, and not
The four combinators express boolean logic over subschemas. They are what make JSON Schema expressive enough to model unions, mixins, and mutual exclusion.
allOf— the data must satisfy every subschema. Useful for composing a base schema with extra constraints (a form of inheritance).anyOf— the data must satisfy at least one subschema. Good for permissive unions where overlap is fine.oneOf— the data must satisfy exactly one subschema. Use it when the alternatives are mutually exclusive, like a discriminated union.not— the data must not satisfy the subschema. Handy for exclusions, e.g. "any string except these reserved words."
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"oneOf": [
{
"properties": {
"kind": { "const": "card" },
"cardNumber": { "type": "string", "pattern": "^[0-9]{16}$" }
},
"required": ["kind", "cardNumber"]
},
{
"properties": {
"kind": { "const": "bank" },
"iban": { "type": "string" }
},
"required": ["kind", "iban"]
}
]
}Reuse with $ref and $defs
Real schemas repeat structures — an address, a timestamp, a money amount. Rather than copy those definitions, you declare them once under $defs and point at them with $ref. A $ref is a URI reference; the fragment #/$defs/address is a JSON Pointer into the current document.
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://example.com/order.json",
"type": "object",
"properties": {
"billingAddress": { "$ref": "#/$defs/address" },
"shippingAddress": { "$ref": "#/$defs/address" }
},
"required": ["billingAddress"],
"$defs": {
"address": {
"type": "object",
"properties": {
"street": { "type": "string" },
"city": { "type": "string" },
"postalCode": { "type": "string" }
},
"required": ["street", "city"]
}
}
}$defs is the standard container for reusable subschemas (it replaced the older definitions keyword). References can also cross documents — "$ref": "https://example.com/address.json" pulls in a schema by its $id. Recursive structures like trees work too, since a $ref can point back at the schema that contains it.
A Worked Schema and Validation Example
Here is a complete user schema that exercises most of the keywords above, followed by validation in both JavaScript and Python.
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://example.com/user.schema.json",
"title": "User",
"type": "object",
"properties": {
"id": { "type": "string", "format": "uuid" },
"email": { "type": "string", "format": "email" },
"age": { "type": "integer", "minimum": 0, "maximum": 150 },
"role": { "enum": ["admin", "editor", "viewer"] },
"tags": {
"type": "array",
"items": { "type": "string" },
"uniqueItems": true
}
},
"required": ["id", "email", "role"],
"additionalProperties": false
}In JavaScript, Ajv is the de facto validator. Note the explicit ajv-formats plugin — without it, format is ignored.
import Ajv from "ajv/dist/2020.js";
import addFormats from "ajv-formats";
import schema from "./user.schema.json" with { type: "json" };
const ajv = new Ajv({ allErrors: true });
addFormats(ajv);
const validate = ajv.compile(schema);
const user = {
id: "7d4e0f1a-1b2c-4d3e-8f90-abcdef123456",
email: "dev@example.com",
role: "editor",
tags: ["beta"],
};
if (validate(user)) {
console.log("valid");
} else {
console.log(validate.errors);
}import json
from jsonschema import Draft202012Validator
with open("user.schema.json") as f:
schema = json.load(f)
validator = Draft202012Validator(schema)
user = {
"id": "7d4e0f1a-1b2c-4d3e-8f90-abcdef123456",
"email": "dev@example.com",
"role": "editor",
}
errors = sorted(validator.iter_errors(user), key=lambda e: e.path)
for error in errors:
print(f"{list(error.path)}: {error.message}")
if not errors:
print("valid")Because additionalProperties is false, adding an unexpected field like isAdmin: true produces a clear error instead of passing silently — exactly what you want when hardening an API against overposting.
Generating a Schema from Existing Data
Writing a schema by hand for a large payload is tedious. The faster path is to infer a first draft from a representative JSON sample, then tighten it. Paste your JSON into the generator below and it produces a draft 2020-12 schema with types, properties, and required fields already filled in.
§JSON Schema GeneratorOpen the tool→Treat the generated output as a starting point, not a finished contract. An inferred schema is only as good as its sample: it cannot know that age should be capped at 150, that role is an enum, or that additionalProperties should be false. After generating, review it and add the semantic constraints — bounds, patterns, enums, formats — that a single example cannot reveal. If your JSON is messy, clean it with the JSON Formatter and confirm it parses with the JSON Validator first.