DevTools Hub

Search tools

Search for a developer tool

JSON Schema Validation Explained

Part of the JSON Toolkit

What Is JSON Schema? covers the vocabulary, and JSON Schema Examples is a reference for common shapes. This one is about the mechanics underneath both: how a validator actually walks a schema against a document, reuses schemas without repeating them, and handles rules that depend on the data itself.

Every applicable keyword is checked — nothing short-circuits

A schema isn't a sequence of steps that stops at the first failure the way an if chain would. A validator checks every keyword that applies to a value and collects whatever fails. Whether you see one error or all of them is a configuration choice, not a property of the schema itself — this is exactly the allErrors option this site's own validator runs with true, on a document missing two required fields at once:

// allErrors: false — stops after the first failure
["must have required property 'a'"]

// allErrors: true — reports everything wrong at once
["must have required property 'a'", "must have required property 'b'"]

Neither is more "correct" — a fast-fail check for a hot code path might reasonably want the first option; a form that should show a user every problem on one submit wants the second.

Reusing schemas with $ref and $defs

Repeating the same sub-schema in multiple places is a maintenance trap — fix a constraint in one copy and forget the other. $defs holds reusable schema fragments, $ref points at one by JSON Pointer:

{
  "type": "object",
  "properties": {
    "billing": { "$ref": "#/$defs/address" },
    "shipping": { "$ref": "#/$defs/address" }
  },
  "$defs": {
    "address": {
      "type": "object",
      "properties": { "city": { "type": "string" }, "zip": { "type": "string" } },
      "required": ["city", "zip"]
    }
  }
}

Both billing and shipping get the exact same validation rules from one definition — change address once and both update. This is also how OpenAPI documents structure reusable request/response shapes at scale, just with #/components/schemas/... as the pointer instead of #/$defs/....

Self-referencing schemas for recursive data

A $ref can point back at the schema it's already inside — necessary for anything tree-shaped, like a comment that can have replies which are themselves comments:

{
  "type": "object",
  "properties": {
    "text": { "type": "string" },
    "replies": { "type": "array", "items": { "$ref": "#" } }
  },
  "required": ["text"]
}

# means "the root schema" — so each reply is validated against the exact same rules as the top-level comment, to whatever depth the data actually nests. No separate schema per nesting level, and no hardcoded recursion limit in the schema itself.

Conditional rules with if / then / else

Sometimes which fields are required depends on the value of another field — a classic case for a plain required array, which has no concept of "required only when." if/then/else covers it:

{
  "type": "object",
  "properties": { "businessType": { "enum": ["individual", "company"] } },
  "required": ["businessType"],
  "if": { "properties": { "businessType": { "const": "company" } } },
  "then": { "required": ["taxId"] },
  "else": { "required": ["ssn"] }
}

{"businessType": "company", "taxId": "..."} passes; {"businessType": "company"} fails then's required check; {"businessType": "individual"} without ssn fails else's instead. if on its own asserts nothing — it's purely the condition that decides whether then or else applies.

Annotation keywords describe; they don't restrict

Not every keyword affects whether data passes. title, description, default, and examples are annotations — metadata for humans and tooling (an editor's tooltip, a generated form's placeholder) that a validator reports back but never enforces:

{
  "type": "string",
  "title": "Name",
  "default": "Ada Lovelace",
  "examples": ["Ada", "Grace"]
}

Any string passes this schema — including one that matches neither the default nor either examples entry. Only type (and anything else that is an assertionrequired, minLength, pattern, and the like) actually constrains the value. It's a common first assumption that examples works like enum; it doesn't — reach for enum if you actually mean to restrict the value to that list.

Try it yourself

JSON Schema Validator supports every keyword covered here, on both Draft-07 and 2020-12. Paste a schema using $ref, if/then/else, or annotations, and check its behavior against real data — entirely in your browser.

Related tools