JSON Schema Versioning: Migrate from Draft-07 to 2020-12
Last updated:
JSON Schema has four widely-used drafts — Draft-04, Draft-06, Draft-07, and Draft 2020-12 (formerly Draft-08) — each adding new keywords and changing a few existing ones. Ajv 8.x defaults to Draft 2020-12; Ajv 6.x defaults to Draft-07. Most breaking changes between Draft-07 and 2020-12 are additive: items → prefixItems, definitions → $defs, and nullable (an OpenAPI extension) removed.
This guide covers the keyword changes between drafts, how to declare which draft you're using with $schema, and a step-by-step migration checklist from Draft-07 to Draft 2020-12.
JSON Schema Draft History: Draft-04 to 2020-12
JSON Schema has evolved through five major drafts since 2013. Draft-04 was the first widely-adopted version, introducing $ref, definitions, and the core validation keywords. Draft-06 (2017) added $id, renamed id to $id, and changed exclusiveMinimum from a boolean to a number. Draft-07 (2018) added if/then/else, readOnly, writeOnly, and $comment — it remains the most widely deployed draft as of 2024 because OpenAPI 3.0 built on a subset of it.
Draft 2019-09 (also known as Draft-08) introduced $defs to replace definitions, added unevaluatedProperties and unevaluatedItems as preview keywords, and introduced $recursiveRef/$recursiveAnchor for extensible recursive schemas. Draft 2020-12 (October 2021) finalized unevaluatedProperties and unevaluatedItems, replaced $recursiveRef with $dynamicRef, and — most visibly — replaced the array form of items with prefixItems.
| Draft | Year | Key additions | Status |
|---|---|---|---|
| Draft-04 | 2013 | $ref, definitions, core keywords | Outdated |
| Draft-06 | 2017 | $id, numeric exclusiveMinimum | Outdated |
| Draft-07 | 2018 | if/then/else, readOnly | Legacy — very common |
| Draft 2019-09 | 2019 | $defs, unevaluatedProperties (preview) | Stable |
| Draft 2020-12 | 2021 | prefixItems, $dynamicRef, finalized unevaluated* | Current — recommended |
The $schema Keyword: How Validators Select a Dialect
The $schema keyword tells validators which draft dialect to use when evaluating a schema. It must be a URI pointing to the official meta-schema for the desired draft. Omitting $schema leaves the choice to the validator — Ajv 6 defaults to Draft-07, while Ajv 8 with the ajv/dist/2020 import defaults to Draft 2020-12. Always declare $schema explicitly so validation behavior is deterministic regardless of which validator version your consumers use.
// Draft-07
{ "$schema": "http://json-schema.org/draft-07/schema#" }
// Draft 2019-09
{ "$schema": "https://json-schema.org/draft/2019-09/schema" }
// Draft 2020-12 (recommended for new schemas)
{ "$schema": "https://json-schema.org/draft/2020-12/schema" }
// Note: no trailing # in 2019-09 and 2020-12 URIsValidators use the $schema URI to load the correct meta-schema and apply the appropriate set of keywords. If you set $schema to a Draft 2020-12 URI but your validator only supports Draft-07, it will either ignore unknown keywords (lenient mode) or throw an error (strict mode). Test your schema against a known validator such as Jsonic's JSON Schema Validator to confirm the correct draft is active.
Breaking Changes from Draft-07 to 2020-12
There are two hard breaking changes and one notable removal between Draft-07 and Draft 2020-12. Understanding each is essential before running any schema through Ajv 8 in 2020-12 mode.
1. Array form of items replaced by prefixItems
In Draft-07, items accepted an array of schemas to validate each array element at a specific index (tuple validation). In Draft 2020-12, this array form is removed. Tuple validation moves to prefixItems. Migration: rename items: [{ ... }] to prefixItems: [{ ... }]. The single-schema form of items remains but now applies only to elements beyond the prefixItems list.
// Draft-07 tuple (items as array)
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "array",
"items": [
{ "type": "string" },
{ "type": "number" },
{ "type": "boolean" }
],
"additionalItems": false
}
// Draft 2020-12 equivalent
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "array",
"prefixItems": [
{ "type": "string" },
{ "type": "number" },
{ "type": "boolean" }
],
"items": false
}2. additionalItems removed
additionalItems — which controlled extra elements after a tuple — is removed in Draft 2020-12. Its role is taken over by the single-schema form of items. Migration: additionalItems: false becomes items: false; additionalItems: { ... } becomes items: { ... }.
3. exclusiveMinimum and exclusiveMaximum (Draft-04 to Draft-06)
In Draft-04, exclusiveMinimum was a boolean that toggled whether minimum was exclusive. Draft-06 changed both to plain numbers. This is a Draft-04 to Draft-06 breaking change, not Draft-07 to 2020-12 — but worth knowing if you have schemas that originate from Draft-04.
// Draft-04 (boolean exclusiveMinimum)
{ "minimum": 0, "exclusiveMinimum": true } // means value > 0
// Draft-06+ (numeric exclusiveMinimum — direct value)
{ "exclusiveMinimum": 0 } // means value > 0New Keywords in Draft 2020-12
Draft 2020-12 introduces five new keywords. Two replace older mechanisms (prefixItems and $dynamicRef/$dynamicAnchor); three are additive and improve correctness for composed schemas (unevaluatedItems, unevaluatedProperties).
| Keyword | What it does | Replaces |
|---|---|---|
prefixItems | Validates array items at specific positions (tuple) | Array form of items |
unevaluatedItems | Validates array items not evaluated by any subschema | Augments items: false |
unevaluatedProperties | Validates object properties not evaluated by any subschema | Augments additionalProperties |
$dynamicRef | Runtime-resolved reference for extensible recursive schemas | $recursiveRef (2019-09) |
$dynamicAnchor | Declares a named hook for $dynamicRef resolution | $recursiveAnchor (2019-09) |
// unevaluatedProperties fixes the allOf + additionalProperties blind spot
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"allOf": [
{ "properties": { "name": { "type": "string" } } }
],
"unevaluatedProperties": false
}
// { "name": "Alice" } → Valid (name was evaluated by allOf)
// { "name": "Alice", "age": 30 } → Invalid (age was never evaluated)Step-by-Step Migration Checklist: Draft-07 to 2020-12
Work through the list in order. Steps 1–4 are required for correctness; steps 5–6 are recommended improvements. Most schemas only need steps 1–3.
| # | Action | Priority |
|---|---|---|
| 1 | Update $schema to "https://json-schema.org/draft/2020-12/schema" | Required |
| 2 | Rename array form of items → prefixItems | Required (if tuples used) |
| 3 | Replace additionalItems: false with items: false; additionalItems: schema with items: schema | Required (if additionalItems used) |
| 4 | Update Ajv import: import Ajv2020 from 'ajv/dist/2020' and use new Ajv2020() | Required (for Ajv users) |
| 5 | Replace definitions with $defs and update $ref paths from #/definitions/ to #/$defs/ | Recommended |
| 6 | Replace additionalProperties: false with unevaluatedProperties: false in schemas that use allOf/$ref composition | Recommended |
To verify the migration, run your schemas through Jsonic's JSON Schema Validator with Draft 2020-12 selected, then validate your existing data samples — all previously valid data should still pass, and all previously invalid data should still fail.
Ajv Version Migration: Ajv 6 (Draft-07) to Ajv 8 (2020-12)
Ajv 8 ships Draft 2020-12 support in a separate module export and enables strict mode by default. Three rules cover most migration scenarios.
// Ajv 6 (Draft-07)
import Ajv from 'ajv'
const ajv = new Ajv()
// Ajv 8 targeting Draft-07 (same API, strict mode by default)
import Ajv from 'ajv'
const ajv = new Ajv({ strict: false }) // disable strict if schemas use unknown keywords
// Ajv 8 targeting Draft 2020-12
import Ajv2020 from 'ajv/dist/2020'
import addFormats from 'ajv-formats'
const ajv = new Ajv2020({ allErrors: true })
addFormats(ajv) // adds email, uri, date, date-time formats etc.
const validate = ajv.compile(schema)
const valid = validate(data)
if (!valid) console.log(validate.errors)Ajv 8 strict mode throws errors for: unknown keywords, floating-point multipleOf, overriding compiled schemas, and ambiguous pattern handling. If your Draft-07 schemas trigger strict-mode errors, pass { strict: false } to suppress them while you audit the schema. See the Ajv JSON Schema validator guide for the full list of strict-mode options and custom keyword patterns.
// Supporting Draft-07 and 2020-12 simultaneously
import Ajv from 'ajv'
import Ajv2020 from 'ajv/dist/2020'
const ajv7 = new Ajv()
const ajv2020 = new Ajv2020()
function getValidator(schema: Record<string, unknown>) {
const draft = String(schema.$schema ?? '')
return draft.includes('2020-12') ? ajv2020 : ajv7
}
// Usage
const v = getValidator(mySchema)
const validate = v.compile(mySchema)
console.log(validate(myData))OpenAPI 3.0 (Draft-07 Subset) vs OpenAPI 3.1 (2020-12)
OpenAPI 3.0 used a strict subset of JSON Schema Draft-07 with several additions and restrictions. The most used addition was nullable: true — a non-standard boolean extension that allowed a field to also accept null. OpenAPI 3.1 dropped nullable in favor of full JSON Schema 2020-12 compatibility, which expresses nullability through a type array.
// OpenAPI 3.0 — nullable extension (not standard JSON Schema)
{
"type": "string",
"nullable": true
}
// OpenAPI 3.1 / JSON Schema 2020-12 equivalent (type array)
{
"type": ["string", "null"]
}
// Alternative using oneOf
{
"oneOf": [
{ "type": "string" },
{ "type": "null" }
]
}OpenAPI 3.1 also allows $defs (in addition to #/components/schemas), supports prefixItems for tuple arrays, and permits all 2020-12 keywords including unevaluatedProperties. The discriminator keyword remains as an OpenAPI extension (it is not in the JSON Schema spec itself), but now works alongside 2020-12 keywords. See the JSON Schema and OpenAPI guide for a full comparison of schema support across OpenAPI versions.
| Feature | OpenAPI 3.0 | OpenAPI 3.1 |
|---|---|---|
| JSON Schema base | Draft-07 subset | Draft 2020-12 (fully compatible) |
| Nullable fields | nullable: true extension | type: ["string", "null"] |
| Tuple arrays | items array form | prefixItems |
| Reusable definitions | #/components/schemas only | #/components/schemas + $defs |
| Composition guard | additionalProperties | unevaluatedProperties (new) |
Key Term Definitions
- JSON Schema Draft
- A versioned specification of the JSON Schema language. Each draft adds or changes keywords, meta-schema URIs, and validation semantics. Drafts are not backward-compatible — validators must know which draft a schema targets to evaluate it correctly.
- $schema URI
- The value of the
$schemakeyword — a URI that identifies the JSON Schema meta-schema for the draft the schema is written against. Validators use it to select the correct set of keyword semantics. Example:https://json-schema.org/draft/2020-12/schema. - Dialect
- A specific instantiation of JSON Schema vocabulary defined by a meta-schema URI. A dialect can be a full draft (e.g., Draft 2020-12) or a subset (e.g., the OpenAPI 3.0 schema dialect). Declaring
$schemaactivates a specific dialect. - Vocabulary
- In Draft 2019-09 and 2020-12, a vocabulary is a named group of keywords defined by a URI. The core, validation, applicator, and unevaluated vocabularies are published by the JSON Schema organization. Custom vocabularies allow domain-specific keyword extensions that validators can opt in to supporting.
- $defs
- The canonical keyword (introduced in Draft 2019-09) for storing named, reusable schema fragments within a schema document. Replaces the older
definitionskeyword. Referenced via$ref: "#/$defs/Name". The keyword itself validates nothing — it is only used when explicitly referenced. - prefixItems
- A Draft 2020-12 keyword that accepts an array of schemas, each validating the array element at the corresponding index. Replaces the array form of
itemsfor tuple validation. Elements beyond the end of theprefixItemsarray are governed by theitemskeyword (orunevaluatedItems). - unevaluatedProperties
- A Draft 2020-12 keyword that restricts object properties not already evaluated by any other keyword in the full schema annotation tree — including properties declared inside
allOf,anyOf,$ref, andif/then/elsebranches. Fixes the blind spot ofadditionalPropertiesin composed schemas.
FAQ
What is the difference between JSON Schema Draft-07 and Draft 2020-12?
Draft 2020-12 introduces two breaking changes from Draft-07: the array form of items is replaced by prefixItems for tuple validation, and additionalItems is removed (use the single-schema items instead). Additive new keywords include unevaluatedItems, unevaluatedProperties, $dynamicRef, and $dynamicAnchor. The $defs keyword (replacing definitions) was introduced in Draft 2019-09 and both work in Ajv 8. See the JSON Schema Draft 2020-12 new keywords guide for a deep dive on every addition.
How do I declare which JSON Schema draft my schema uses?
Add $schema at the root of your schema with the draft URI. For Draft-07: "$schema": "http://json-schema.org/draft-07/schema#". For Draft 2020-12: "$schema": "https://json-schema.org/draft/2020-12/schema". Always declare it — omitting $schema lets validators pick their own default, which differs between Ajv 6 (Draft-07) and Ajv 8 with ajv/dist/2020.
How do I migrate from JSON Schema Draft-07 to Draft 2020-12?
Four steps: (1) Update $schema to the 2020-12 URI. (2) Rename the array form of items to prefixItems. (3) Replace additionalItems with the single-schema items. (4) Update your Ajv import to import Ajv2020 from 'ajv/dist/2020'. Optionally rename definitions to $defs and switch additionalProperties: false to unevaluatedProperties: false where you use allOf composition. Validate your data samples after each step to catch regressions.
What changed with the items keyword in JSON Schema 2020-12?
In Draft-07, items accepted either a single schema (all items) or an array of schemas (tuple). In Draft 2020-12, the array form is gone. Tuple validation uses prefixItems. The single-schema items remains but now only applies to array elements beyond the count of prefixItems entries. Set items: false to disallow any extra elements; set items: { "type": "string" } to require extra elements to be strings.
How do I update Ajv from version 6 to version 8?
Run npm install ajv@8 ajv-formats. For Draft-07 schemas, use import Ajv from 'ajv' and add { strict: false } if your schemas contain unknown keywords. For Draft 2020-12 schemas, use import Ajv2020 from 'ajv/dist/2020'. Ajv 8 strict mode is on by default and will error on schemas with unknown or ambiguous patterns — review the error message and fix the offending keyword. See the full Ajv JSON Schema validator guide.
Can I use Draft-07 schemas with Ajv 8?
Yes. The default import (import Ajv from 'ajv') in Ajv 8 targets Draft-07. Create an instance with new Ajv() and it validates Draft-07 schemas exactly as Ajv 6 did (with the caveat that strict mode may flag issues in looser schemas). You can run separate new Ajv() and new Ajv2020() instances simultaneously to support both drafts in the same process.
What is the difference between definitions and $defs?
definitions was the Draft-04/07 keyword for storing named reusable schemas. It was renamed to $defs in Draft 2019-09. Both are functionally identical — they store schemas that only activate when referenced by $ref. The reference path changes: "$ref": "#/definitions/Name" becomes "$ref": "#/$defs/Name". Ajv 8 supports both keywords. For new schemas, always use $defs. See the JSON Schema $ref and $defs guide for multi-file referencing patterns.
Does OpenAPI 3.1 use JSON Schema 2020-12?
Yes. OpenAPI 3.1 is fully compatible with JSON Schema Draft 2020-12, replacing the Draft-07 subset used by OpenAPI 3.0. The main migration change: nullable: true (an OpenAPI 3.0 extension) becomes type: ["string", "null"]. OpenAPI 3.1 also supports prefixItems, $defs, and unevaluatedProperties. The discriminator keyword is retained as an OpenAPI extension alongside standard JSON Schema keywords. Read the JSON Schema and OpenAPI guide for the full migration path.
Validate your migrated schema against real data — paste it into Jsonic's JSON Schema Validator for instant Draft 2020-12 feedback.
Open JSON Schema ValidatorFurther reading and primary sources
- JSON Schema: Understanding Draft 2020-12 — Official release notes covering every keyword change in Draft 2020-12
- JSON Schema: Understanding the Specification — Comprehensive guide to all JSON Schema keywords across drafts
- Ajv Migration Guide (v6 to v8) — Official Ajv guide to breaking changes and new API in version 8
- JSON Schema Draft 2020-12 New Keywords (Jsonic) — prefixItems, unevaluatedProperties, $dynamicRef, and migration examples
- JSON Schema $ref and $defs (Jsonic) — Reusable definitions, recursive schemas, and multi-file references
- OpenAPI 3.1 Migration Guide — Official guide covering nullable migration and JSON Schema 2020-12 adoption