DevDockTools

JSON Schema Draft Versions: Which One Should You Target?

Draft-07, 2019-09, or 2020-12? A practical breakdown of what changed between JSON Schema drafts and which version to target for new projects.

By Daniel Agrici5 min read
json schemajsonvalidationapi designdeveloper tools

Pick the wrong JSON Schema draft and you find out months later, when unevaluatedProperties silently does nothing, or a schema that validates fine in one library gets rejected by another. The draft version isn't cosmetic — it changes what keywords exist, how $ref resolves, and how strict your validation actually is. Here's what changed across the drafts that matter and how to pick one deliberately instead of by accident.

Why the Draft Version Matters

JSON Schema is versioned through "drafts," each identified by a $schema URI like http://json-schema.org/draft-07/schema# or https://json-schema.org/draft/2020-12/schema. The draft you declare determines which keywords are recognized, how schema composition (allOf, anyOf, $ref) behaves, and in some cases what a valid document even looks like. A schema authored for one draft can validate documents incorrectly — or throw errors — when loaded by a validator expecting a different one.

Most confusion comes from three drafts still in active use: draft-07 (2018), 2019-09, and 2020-12, the current stable draft. Draft-06 and earlier are mostly legacy at this point and rarely worth targeting for new work.

What Changed: Draft-07 to 2019-09

Draft-07 is where most existing schemas — and a lot of tooling — still lives. It has if/then/else, const, contains, and format assertions, but its composition model has a real gap: there's no way to say "these are all the properties I've accounted for across a set of combined subschemas."

2019-09 introduced the vocabulary system, splitting the spec into modular pieces (core, validation, applicator, meta-data, format) that implementations can support independently. The practical developer-facing change is unevaluatedProperties and unevaluatedItems, which track what's been validated across allOf, $ref, and conditional branches — something additionalProperties alone can't do when schemas are composed.

{
  "$schema": "https://json-schema.org/draft/2019-09/schema",
  "allOf": [
    { "properties": { "name": { "type": "string" } } }
  ],
  "properties": {
    "age": { "type": "number" }
  },
  "unevaluatedProperties": false
}

With draft-07, additionalProperties: false on the outer schema wouldn't see name as accounted for — it only looks at the local properties keyword, not what sibling allOf branches validated. unevaluatedProperties fixes exactly this composition problem.

What Changed: 2019-09 to 2020-12

2020-12 is largely a cleanup and stabilization pass, but two changes affect real schemas:

  • items and prefixItems split apart. In earlier drafts, items did double duty — an array of schemas meant tuple validation, a single schema meant "every item matches this." 2020-12 separates these: prefixItems is now the tuple form, and items is only ever a single schema applied to remaining elements after the prefix. This removes a common source of ambiguity when reading someone else's schema.
  • $dynamicRef / $dynamicAnchor replaced $recursiveRef / $recursiveAnchor. These support extensible, recursive schema patterns (useful for building schema "base classes"). Most application-level schemas never touch this, but schema-authoring libraries do.
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "array",
  "prefixItems": [
    { "type": "string" },
    { "type": "number" }
  ],
  "items": { "type": "boolean" }
}

That schema validates ["ok", 42, true, false] — the first two positions are fixed types, everything after must be boolean. Under draft-07 this required an object-form items array and a separate additionalItems keyword, which is less discoverable and easy to get wrong.

Which Draft to Target

This is the actual decision point, and it depends on what's consuming the schema, not personal preference:

  • New projects with modern tooling (Ajv 8+, most Python/Go/Rust validators updated in the last couple of years): target 2020-12. It's the current spec, has the clearest composition semantics via unevaluatedProperties, and new tooling development happens against it.
  • You're integrating with an ecosystem that pins an older draft — some API gateways, older OpenAPI tooling (OpenAPI 3.0 uses a draft-04-like subset), or a validator library that hasn't updated — target draft-07. Fighting your tooling to use a newer draft it doesn't fully support isn't worth it.
  • You need unevaluatedProperties but your validator's 2020-12 support is incomplete: 2019-09 is a reasonable middle ground since it introduced the same composition keywords with a smaller feature surface than 2020-12.

Whatever you pick, always declare $schema explicitly. Omitting it means different validators may fall back to different default drafts, and that default can change across a library's major versions without warning. An explicit $schema line is one of the cheapest correctness guarantees you can add to a schema file.

Validating Against Your Chosen Draft

Once you've picked a draft, keeping documents honest against it is a formatting and validation problem you'll hit constantly during development. Paste a schema and a sample document into the JSON Validator to check structural validity, or use the JSON Formatter to clean up and inspect schema files pulled from third-party APIs before you commit to a draft assumption about them.

Migrating an Existing Schema

Moving draft-07 schemas to 2020-12 is usually mechanical:

  1. Update $schema to the 2020-12 URI.
  2. Replace any array-form items (tuple validation) with prefixItems, and change the trailing items to the single-schema form.
  3. Replace additionalItems with items under the new semantics.
  4. Where you were faking "all evaluated properties" with duplicated properties blocks across allOf branches, replace with unevaluatedProperties: false at the composing level.
  5. Re-run your test documents — both valid and intentionally invalid ones — against the updated schema with your validator library, since composition keyword behavior is the part most likely to shift.

Don't do a bulk find-and-replace on $schema alone and call it done — the items/prefixItems split is a silent behavior change, not a syntax error, so a document that used to fail validation might start passing (or vice versa) without any error to flag it.

Frequently Asked Questions

Is JSON Schema draft-07 still safe to use for new projects?
It works and is well supported, but it lacks `unevaluatedProperties` and the vocabulary system, which makes composing schemas with allOf harder. For a new project, 2020-12 is a better default unless a specific tool only supports draft-07.
Which draft do most validator libraries support?
Draft-07 has the broadest support across languages because it has been stable the longest. 2019-09 and 2020-12 support varies by library, so check your specific validator's compatibility table before committing.
Do I need to specify a $schema keyword in every JSON Schema file?
You should. Without an explicit $schema URI, validators fall back to their own default draft, which can silently change validation behavior when you upgrade the library or switch tools.