JSON Schema Basics

JSON Schema describes what a valid JSON document looks like: which keys exist, their types, and the rules their values follow. One schema can validate API requests, power editor autocompletion and generate documentation. This guide covers the parts you will use every day.

A first schema

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://example.com/schemas/order.json",
  "title": "Order",
  "type": "object",
  "properties": {
    "id": { "type": "string", "pattern": "^ord_[a-z0-9]+$" },
    "total": { "type": "number", "minimum": 0 },
    "status": { "enum": ["pending", "paid", "shipped"] },
    "items": {
      "type": "array",
      "minItems": 1,
      "items": { "$ref": "#/$defs/item" }
    }
  },
  "required": ["id", "total", "items"],
  "additionalProperties": false,
  "$defs": {
    "item": {
      "type": "object",
      "properties": {
        "sku": { "type": "string" },
        "qty": { "type": "integer", "minimum": 1 }
      },
      "required": ["sku", "qty"]
    }
  }
}

$schema declares which version of JSON Schema the document uses, and validators pick their rules from it. $id gives the schema a URI so other schemas can reference it. Everything else is a keyword that adds a constraint; a document is valid only if it satisfies every keyword that applies to it.

Types

type takes one of seven names — "string", "number", "integer", "boolean", "object", "array" and "null" — or an array of them, such as ["string", "null"] for a nullable string. "integer" accepts numbers without a fractional part; since draft-06, 1.0 counts as an integer.

A schema without type accepts any type, and keywords only constrain values of the type they are about: minLength is ignored for a number. This catches people out — { "minimum": 0 } alone accepts the string "hello", because minimum only applies to numbers.

Objects and the required trap

  • properties — a schema for each named key
  • required — an array of keys that must be present
  • additionalProperties — false to forbid unlisted keys, or a schema they must match
  • patternProperties — schemas for keys matching a regular expression
  • propertyNames, minProperties, maxProperties
  • dependentRequired — if key A is present, keys B and C must be too (2019-09 and later; earlier drafts used dependencies)

The most common mistake in JSON Schema is assuming that properties makes keys mandatory. It does not: properties only says what a key must look like if it is present. An empty object {} passes a schema that lists ten properties and no required array.

A second trap: additionalProperties: false only sees the properties defined next to it. If you combine schemas with allOf, keys defined in the other branches count as “additional” and are rejected. Drafts 2019-09 and 2020-12 added unevaluatedProperties: false, which looks across allOf, $ref and conditional branches and is the right tool there.

Arrays, strings and numbers

Arrays: items gives the schema for elements, minItems and maxItems bound the length, uniqueItems: true forbids duplicates, and contains requires at least one matching element. For tuples, 2020-12 uses prefixItems for positional schemas and items for the rest; draft-07 and earlier wrote tuples as an array under items with additionalItems for the rest.

Strings: minLength, maxLength, pattern and format. Two details matter. pattern is an ECMA-262 regular expression and is not anchored — "pattern": "[0-9]+" accepts "abc1" — so add ^ and $ when you mean the whole string. And format (date-time, email, uri, uuid, ipv4 and others) is an annotation by default in 2019-09 and 2020-12: many validators only enforce it when format assertion is switched on.

Numbers: minimum, maximum, exclusiveMinimum, exclusiveMaximum and multipleOf. In draft-04 the exclusive keywords were booleans that modified minimum/maximum; from draft-06 on they are numbers in their own right.

Fixed values: enum lists allowed values of any type; const (draft-06 and later) requires exactly one value.

Combining schemas and reuse

  • allOf — must match every subschema (composition)
  • anyOf — must match at least one
  • oneOf — must match exactly one; slower and stricter than anyOf, so use it only when overlap must be an error
  • not — must not match
  • if / then / else (draft-07 and later) — conditional rules, such as requiring vatNumber when country is in the EU

Reusable pieces live under $defs (2019-09 and later; definitions before that) and are referenced with a JSON Pointer: { "$ref": "#/$defs/item" }. $ref can also point to another file by URI. In draft-07 and earlier, any keywords written next to $ref are ignored; from 2019-09 they apply alongside the referenced schema.

Annotation keywords — title, description, default, examples, deprecated, readOnly and writeOnly — do not affect validation but feed documentation and editor tooltips.

Drafts at a glance

  • draft-04 (2013) — still common in older tooling; boolean exclusiveMinimum, id without the dollar sign
  • draft-06 (2017) — const, contains, propertyNames, numeric exclusive bounds, boolean schemas
  • draft-07 (2018) — if/then/else, readOnly/writeOnly; the most widely supported version
  • 2019-09 — $defs, dependentRequired, unevaluatedProperties, $anchor, format as annotation
  • 2020-12 — prefixItems, redefined items, $dynamicRef; the current version

OpenAPI 3.0 uses its own extended subset of an early draft (with nullable: true instead of type: [..., "null"]), while OpenAPI 3.1 is aligned with 2020-12, so a 3.1 schema object is a real JSON Schema.

Schemas in your editor

Editors are where schemas pay off daily. VS Code validates a JSON file against the schema named in its $schema property, and the json.schemas setting maps file patterns to schemas. For YAML, the Red Hat YAML extension reads a first-line comment such as # yaml-language-server: $schema=./order.schema.json. Both also consult the SchemaStore catalogue, which is why package.json, tsconfig.json and GitHub workflow files get autocompletion with no setup at all. Writing description on every property turns those completions into inline documentation.

Validating and generating schemas

PasteKit validates a JSON document against a schema in the browser, choosing draft-04, 06, 07, 2019-09 or 2020-12 rules from the schema’s $schema value, and reports each failure with the JSON Pointer of the failing value so you can jump to its line. It can also check OpenAPI 3.0, 3.1 and Swagger 2.0 documents against the official schemas. To get started quickly, JSON to JSON Schema infers a starting schema from a sample document; tighten it afterwards by adding required, pattern and bounds that a single sample cannot reveal.

Frequently asked questions

Which JSON Schema draft should I use?

Use 2020-12 for new schemas if your validator supports it, and draft-07 when you need the widest tool compatibility. Always declare the version with $schema.

Why does my schema accept objects with missing fields?

properties does not make keys mandatory. List mandatory keys in a required array next to properties.

Is format: email enforced?

Not necessarily. In 2019-09 and 2020-12, format is an annotation unless the validator enables format assertion, so check your validator settings.

What is the difference between $defs and definitions?

They serve the same purpose. $defs is the official name from 2019-09 on; definitions was the conventional location in earlier drafts and still works as a $ref target.

Can JSON Schema validate YAML files?

Yes. YAML maps onto the same data model, so editors and CI tools validate Kubernetes manifests, GitHub workflows and other YAML against JSON Schemas.

Related