{ }jsonkitOpen the tool

JSON Schema: A Complete Guide with Examples

7 min read · updated 2026-07-12

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.

A minimal but complete schema. $schema names the dialect; $id gives the schema a stable identity.json
{
  "$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

KeywordApplies toWhat it does
typeanyRestricts the JSON type: string, number, integer, boolean, object, array, or null. Can be an array of types.
propertiesobjectMaps property names to subschemas that their values must satisfy.
requiredobjectLists property names that must be present (independent of properties).
additionalPropertiesobjectControls properties not named in properties: false forbids them, or a schema constrains them.
itemsarraySchema every array element must match.
enumanyThe value must equal one of a fixed list of allowed values.
constanyThe value must equal exactly this one value.

String, number, and array constraints

KeywordApplies toWhat it does
minLength / maxLengthstringBounds on string length in characters.
patternstringA regular expression the string must match (unanchored by default).
formatstringSemantic hint like email, date-time, uri, uuid — enforced only if the validator opts in.
minimum / maximumnumberInclusive numeric bounds. Use exclusiveMinimum / exclusiveMaximum for strict bounds.
multipleOfnumberThe number must be an exact multiple of this value.
minItems / maxItemsarrayBounds on array length.
uniqueItemsarrayWhen 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."
A payment method that is exactly one of card or bank transfer, distinguished by a const discriminator.json
{
  "$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.

An address subschema defined once and referenced twice.json
{
  "$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.

user.schema.json — a realistic object schema.json
{
  "$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.

Validating with Ajv (npm install ajv ajv-formats).js
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);
}
The same check in Python with the jsonschema package (pip install jsonschema).python
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.

Frequently asked questions

What is the difference between JSON Schema draft-07 and draft 2020-12?

They share nearly all core keywords, but 2020-12 changed array validation (tuples moved from items to prefixItems, and items now applies to every remaining element) and standardized $defs over definitions. Declare your dialect with $schema so validators apply the right rules, and confirm your validator supports 2020-12 before upgrading.

Does JSON Schema validate the format keyword automatically?

Not by default. format (email, date-time, uuid, uri, and so on) is treated as an annotation unless the validator opts into format assertion. In Ajv you must install and register ajv-formats; Python's jsonschema checks some formats only when optional libraries are installed. If a value must match a format, enable it explicitly or add a pattern as backup.

When should I use oneOf versus anyOf?

Use anyOf when the data may satisfy one or more of the alternatives — it passes if at least one matches. Use oneOf only for mutually exclusive cases where exactly one branch should match; it fails if zero or more than one match. Overlapping oneOf branches are a common source of confusing errors, so add a const discriminator to keep them disjoint.

How do I forbid extra properties in a JSON object?

Set additionalProperties: false on the object schema. This rejects any property not listed under properties (or matched by patternProperties). It is the most important keyword for locking down API payloads and preventing overposting, though it also means you must list every allowed field explicitly.

Can I reuse the same subschema in multiple places?

Yes. Define it once under $defs and reference it with $ref using a JSON Pointer like #/$defs/address. References can point within the same document or to another schema by its $id, and they support recursion, so structures like trees and linked lists are expressible.

How do I create a JSON Schema from an existing JSON file?

Infer a starting schema from a representative sample using a generator like the JSON Schema Generator, which outputs draft 2020-12 with types, properties, and required fields filled in. Then refine it by hand — add enums, numeric bounds, patterns, formats, and additionalProperties: false, since a single example cannot capture those rules.