JSON

A Practical Introduction to JSON Schema

A JSON parser tells you whether a document is well-formed. It can't tell you whether it's correct — whether "port" is a number, whether "email" is present, or whether "role" is one of the three values your application actually understands. That's the job of JSON Schema: a vocabulary, itself written in JSON, for describing what valid data looks like.

Rather than list keywords alphabetically, this guide builds a schema step by step for a realistic document and points out the traps along the way. Examples use draft 2020-12, the current version, but everything here also works in the widely deployed draft-07 unless noted.

The document we want to validate

{
  "id": "usr_8f2a",
  "email": "dev@example.com",
  "role": "editor",
  "age": 34,
  "tags": ["beta", "internal"],
  "address": { "city": "Pune", "country": "IN" }
}

Step 1: Declare the type

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object"
}

$schema tells validators which draft's rules to apply. type can be object, array, string, number, integer, boolean, or null — or an array of these, such as ["string", "null"] for a nullable string.

Note that integer is a JSON Schema concept, not a JSON one. JSON only has "number". A validator treats 34 and 34.0 as integers, because they're the same mathematical value.

Step 2: Describe properties

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "id":    { "type": "string", "pattern": "^usr_[a-z0-9]+$" },
    "email": { "type": "string", "format": "email" },
    "role":  { "enum": ["viewer", "editor", "admin"] },
    "age":   { "type": "integer", "minimum": 0, "maximum": 150 },
    "tags":  { "type": "array", "items": { "type": "string" } }
  }
}

This looks complete, but it would accept {} — an empty object. That's the first and most important trap.

Trap 1: properties doesn't make anything required

properties only says "if this key is present, it must match this subschema." To require keys, list them separately:

"required": ["id", "email", "role"]

Keep required at the object level, next to properties. Putting "required": true inside an individual property was draft-03 syntax and is silently ignored by modern validators.

Trap 2: Unknown keys are allowed by default

Without further instruction, a schema accepts any extra properties. A typo like "emial" sails through as an unknown key while the real email is missing — and if email isn't required, nothing fails at all.

"additionalProperties": false

This rejects any key not listed in properties. Use it for configuration files and request bodies you fully control. Leave it off for data that's expected to grow, such as responses from a third-party API that may add fields in future versions — otherwise your validation breaks every time they ship a new field.

Trap 3: format may not be enforced

In draft 2019-09 and later, format is an annotation by default: validators may record it but aren't required to check it. Some libraries validate formats like email, date-time, and uri only when you explicitly enable it — Ajv, for example, needs the separate ajv-formats package. If format validation matters, test that an obviously invalid value actually fails.

Step 3: Nested objects

A property's subschema can be a full object schema of its own:

"address": {
  "type": "object",
  "properties": {
    "city":    { "type": "string", "minLength": 1 },
    "country": { "type": "string", "pattern": "^[A-Z]{2}$" }
  },
  "required": ["country"],
  "additionalProperties": false
}

Note that required and additionalProperties apply per object. Setting them on the outer schema has no effect on address.

Step 4: Arrays

"tags": {
  "type": "array",
  "items": { "type": "string", "minLength": 1 },
  "uniqueItems": true,
  "maxItems": 20
}

items applies one schema to every element. For fixed-position arrays (tuples), draft 2020-12 uses prefixItems; draft-07 used an array-valued items for the same purpose. This is one of the few places where copying an older example into a 2020-12 schema changes its meaning.

Step 5: Reuse with $defs and $ref

Once the same shape appears twice — say, a billing and a shipping address — define it once:

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$defs": {
    "address": {
      "type": "object",
      "properties": {
        "city":    { "type": "string" },
        "country": { "type": "string", "pattern": "^[A-Z]{2}$" }
      },
      "required": ["country"]
    }
  },
  "type": "object",
  "properties": {
    "billing":  { "$ref": "#/$defs/address" },
    "shipping": { "$ref": "#/$defs/address" }
  }
}

In draft-07 the container was named definitions. The $ref mechanism is the same.

Conditional rules

Real data has rules like "if role is admin, then mfaEnabled is required." JSON Schema expresses this with if / then:

"if":   { "properties": { "role": { "const": "admin" } }, "required": ["role"] },
"then": { "required": ["mfaEnabled"] }

The "required": ["role"] inside if matters. Without it, a document with no role at all matches the if — because properties doesn't require anything, as in Trap 1 — and the then clause is applied unexpectedly.

Starting from a sample instead of a blank page

Writing a schema from scratch for a large payload is tedious. A faster path is to infer a draft schema from a real sample document, then tighten it by hand: add required, turn free-text fields into enums where the values are fixed, and decide deliberately where additionalProperties should be false. Inference can only describe the sample it's given — it can't know that role has exactly three legal values, or that tags may legitimately be empty — so treat its output as a first draft, not a finished contract.

Where to use JSON Schema

  • API request validation — reject bad input at the edge with a clear error, before it reaches business logic.
  • Configuration files — many editors, VS Code included, can use a schema to offer autocomplete and inline errors for a JSON config file.
  • Contract tests — validate real API responses against a schema in CI to catch breaking changes early.
  • OpenAPI — OpenAPI 3.1 uses JSON Schema 2020-12 directly for request and response bodies, so anything you learn here carries over.

Checklist

  • Every object schema has an explicit required list.
  • You've decided deliberately whether each object allows additional properties.
  • You've confirmed your validator actually enforces format, if you rely on it.
  • Repeated shapes live in $defs.
  • You've tested the schema against at least one document that should fail, not only ones that should pass.

Before any of this, the document has to parse. If it doesn't, start with the 10 JSON syntax errors you'll actually hit.

Try this in JSON Editor Pro

Format, validate, compare, and explore JSON — entirely in your browser.

Keep reading

More JSON guides