JSON Schema answers a deceptively simple question: "is this JSON the JSON we agreed on?" A schema is itself just JSON — a handful of keywords describing shapes, types, and limits — yet it powers API validation, config checking, form generation, and documentation across most of the industry. Here's the mental model, the vocabulary that matters, and the draft-version traps that bite in production.
Take a user object. A schema for it might read:
{"type":"object","properties":{"name":{"type":"string","minLength":1},"age":{"type":"integer","minimum":18},"email":{"type":"string"}},"required":["name","email"],"additionalProperties":false}
Read it aloud: it's an object; if name appears it's a non-empty string; age, if present, is an integer of at least 18; name and email must be present; nothing else is allowed. Feed that schema and any document to a validator and you get a verdict — plus, when it fails, a list of exactly where and why. That's the whole discipline. Everything else is vocabulary.
| Keyword | What it enforces | Example |
|---|---|---|
type | The JSON type: object, array, string, number, integer, boolean, null | {"type":"integer"} |
properties | Per-key subschemas for object members | {"properties":{"age":{"minimum":0}}} |
required | Keys that must exist (any value) | {"required":["id"]} |
items | Schema every array element must match | {"items":{"type":"string"}} |
enum | Exact allowed values | {"enum":["red","green"]} |
minimum / maximum | Numeric bounds (inclusive) | {"minimum":18} |
minLength / maxLength | String length bounds | {"maxLength":280} |
pattern | ECMAScript regex the string must match | {"pattern":"^\\\\d{5}$"} |
format | Well-known shape hint (email, date, uuid) | {"format":"date-time"} |
additionalProperties | Forbid or constrain stray keys | {"additionalProperties":false} |
Two composition keywords deserve special mention because they replace hand-written conditional logic. anyOf accepts a document matching any listed subschema (a value that's either a string or null — the classic nullable-field problem). oneOf demands exactly one match, which is stricter and a common source of accidental failures when two branches overlap. allOf merges constraints and is the standard way to compose a base schema with per-endpoint extras.
properties only constrains keys that appear; required is the must-exist list. Missing from both? Unconstrained.integer vs number. 2.0 satisfies integer; 2.5 doesn't. In draft-07 and later, "integer" means no fractional part, and JSON parsers keep 2.0 as a float in most languages — validators handle it, your hand-rolled checks might not.format is advisory by default. The spec says validators may ignore it, and Ajv 8 does unless formats are registered — so "format":"email" alone can silently pass garbage.pattern searches, it doesn't full-match. Pin your patterns with ^ and $.JSON Schema has no releases in the npm sense; it has dialects, declared via $schema. The three you'll meet:
$schema is present is still it. Reusable chunks live under definitions.$defs replaces definitions, $ref stops ignoring sibling keywords, and vocabularies formalize extension. Adoption stayed thin.prefixItems, items becomes the tail schema, and unevaluatedProperties catches keys nothing claimed.The practical damage shows up at API boundaries: a 3.0-era schema dropped into a 3.1 toolchain can misfire on $ref composition or items handling, and the error surfaces as a validation anomaly rather than a version complaint. When something validates in one tool and not another, $schema is the first thing to check.
A good validator doesn't say "invalid." It says where and why, using three fields per error: an instance path (JSON Pointer into your document, like /items/2/price), a schema path (which keyword fired, like #/properties/price/minimum), and a message with keyword params. Ajv — the library behind most Node.js validation stacks — compiles your schema to a JavaScript function for speed and returns exactly this structure. Turn on allErrors or you'll fix problems one render at a time.
Paste a schema and a document, pick a dialect, and see every violation with its pointer path — all in your browser.
Open the JSON Schema Validator →Writing schemas by hand is fine for small contracts. For bigger surfaces, generate a starting point and edit: many tools infer a schema from sample documents, database rows, or TypeScript types. The result won't be tight — generators err permissive — but it gives you the property skeleton, and you add required, bounds, and enums where the business rules live. Once the schema exists, keep it in the repo next to the code it guards and let CI validate your fixtures against it, so drift gets caught at pull-request time.
Working with schemas day to day pairs naturally with the rest of the JSON toolchain: our JSON Schema validator runs the checks, the JSON formatter untangles minified payloads while you debug, and the YAML validator handles the config files that need the same discipline in YAML form. Converting tabular exports to test fixtures? CSV to JSON closes that loop.
Four jobs, mostly: validating API request and response bodies before your code trusts them, documenting payload shapes in one machine-readable place, generating forms and client code from the contract, and guarding config files at deploy time instead of discovering a typo in production. One schema can serve all four, which is why it beats ad-hoc validation functions that drift from the docs.
Yes — a schema is a JSON document describing other JSON documents. That means you can store it next to your code, serve it from an endpoint, diff it in code review, and validate it against the spec's own meta-schema to catch typos like reqired before they do damage.
Follow your ecosystem. OpenAPI 3.0 is built on draft-07, OpenAPI 3.1 on 2020-12, and Ajv 8 supports draft-07, 2019-09, and 2020-12 out of the box. If nothing forces your hand, draft-07 remains the safest interop choice because every tool understands it; pick 2020-12 when you want prefixItems, unevaluatedProperties, or full $ref composition.
properties declares what a key must look like if it's present; required lists keys that must be present. A field can appear in required without appearing in properties — it's then mandatory but unconstrained. To demand a field and constrain it, list it in both. This split trips up nearly everyone writing their first schema.
Draft-04 used booleans alongside minimum (exclusiveMinimum: true). Draft-06 and later replaced that with a number (exclusiveMinimum: 18), dropping the companion keyword. Ajv 8 rejects the boolean form as invalid for modern dialects, so a schema migrated between eras can fail compilation with a confusing message — the fix is rewriting it as the numeric form.