Why 2020-12 is the draft that matters
JSON Schema has gone through many drafts, and for years tooling lagged behind the spec. Draft 2020-12 is where the ecosystem converged: it's what modern validators default to, and — crucially — it's the dialect OpenAPI 3.1 aligned with, ending years of "OpenAPI's schema is *almost* JSON Schema" pain. If you're writing new schemas in 2026, target 2020-12. This guide covers what changed and the keywords people most often get wrong.
Declare the dialect at the top so validators pick the right rules:
{ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object" }The big change: prefixItems replaces the old array keywords
The most breaking change from older drafts is how arrays and tuples work. Previously, items did double duty — an object meant "every element matches this," an array meant "positional tuple." That overloading was confusing, so 2020-12 split it:
- `prefixItems` — an array of schemas for positional (tuple) validation.
- `items` — now *only* the schema for elements beyond the prefix (what
additionalItemsused to do).
{
"type": "array",
"prefixItems": [
{ "type": "number" },
{ "type": "string" }
],
"items": { "type": "boolean" }
}This validates [1, "hi", true, false]: position 0 is a number, position 1 a string, and every element after follows items. additionalItems is gone — if you see it, the schema is pre-2020-12.
$dynamicRef and $dynamicAnchor
2020-12 replaced the confusing $recursiveRef/$recursiveAnchor with `$dynamicRef` and `$dynamicAnchor`. These enable *extensible recursion* — a base schema can reference a placeholder that a schema extending it fills in later. It's an advanced feature you mostly meet in schema libraries (like a generic "tree" schema specialized per use), but knowing the rename saves confusion when reading modern meta-schemas.
anyOf vs oneOf: the distinction people get wrong
These look similar and behave differently:
- `anyOf` — valid if the data matches at least one subschema (logical OR). This is what you want the vast majority of the time.
- `oneOf` — valid only if the data matches exactly one subschema (exclusive XOR). If two subschemas both match,
oneOf*fails*.
{ "anyOf": [ { "type": "string" }, { "type": "number" } ] }The classic oneOf trap: writing { "type": "string" } and { "maxLength": 5 } as alternatives — a 3-character string matches both, so oneOf rejects it. Unless you genuinely need mutual exclusivity, reach for anyOf.
if / then / else: conditional validation
Conditional schemas apply different rules based on the data. The pattern reads exactly as it looks:
{
"type": "object",
"properties": { "country": { "type": "string" }, "postalCode": { "type": "string" } },
"if": { "properties": { "country": { "const": "US" } } },
"then": { "properties": { "postalCode": { "pattern": "^[0-9]{5}$" } } },
"else": { "properties": { "postalCode": { "minLength": 3 } } }
}If country is US, the postal code must be five digits; otherwise a looser rule applies. if never produces errors itself — it only decides whether then or else applies.
Writing and validating 2020-12 schemas
- Draft a realistic sample and generate a starting schema from it, then tighten it with
required,enum, and conditionals. - Validate data against your schema to confirm your
anyOf/oneOfandif/then/elsebehave as intended — theoneOfoverlap trap is easy to miss by eye. - Format the schema so deeply nested
prefixItemsand$defsstay readable.
For fundamentals start with the JSON Schema Guide; for reuse patterns see JSON Schema $ref and definitions.