JSON Schema validation: a practical guide

Updated 22 October 2026 · first published 15 January 2026 · 8 min read · By the JSON XML Editor team

JSON Schema describes what valid JSON looks like: which fields exist, their types, which are required and what values are allowed. Validators then check data against it, so bad requests are rejected with a clear message instead of failing somewhere deep in your code. The same schema documents your API and can generate types and forms.

This guide uses the current version, draft 2020-12, which OpenAPI 3.1 also uses.

A first schema

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "name": { "type": "string", "minLength": 1 },
    "age": { "type": "integer", "minimum": 0 }
  },
  "required": ["name"],
  "additionalProperties": false
}

{"name": "Ada", "age": 36} is valid. {"age": -1} fails twice (no name, negative age), and {"name": "Ada", "nickname": "A"} fails because additionalProperties: false forbids unknown fields.

Generate a starting schema from a sample with the JSON Schema generator, then tighten it by hand.

Strings

{
  "username": { "type": "string", "minLength": 3, "maxLength": 20, "pattern": "^[a-zA-Z0-9_]+$" },
  "email": { "type": "string", "format": "email" },
  "status": { "enum": ["draft", "active", "archived"] }
}

One surprise: in 2020-12, format is an annotation by default. Many validators only enforce it when you switch format checking on (for example the ajv-formats plugin in Ajv). Use pattern when the check must always run.

Numbers

{
  "price": { "type": "number", "exclusiveMinimum": 0, "multipleOf": 0.01 },
  "quantity": { "type": "integer", "minimum": 1, "maximum": 100 }
}

integer rejects 1.5; number accepts both. Note that multipleOf: 0.01 can misjudge some decimals in validators that use floating point, so prices are often sent as integer cents instead.

Arrays

{
  "tags": { "type": "array", "items": { "type": "string" }, "minItems": 1, "maxItems": 5, "uniqueItems": true },
  "point": { "type": "array", "prefixItems": [{ "type": "number" }, { "type": "number" }], "items": false }
}

items applies to every element. For tuples (fixed positions), 2020-12 uses prefixItems; "items": false then forbids extra elements.

Conditional rules

{
  "if": { "properties": { "country": { "const": "US" } }, "required": ["country"] },
  "then": { "properties": { "postcode": { "pattern": "^[0-9]{5}$" } } },
  "else": { "properties": { "postcode": { "type": "string" } } }
}

Put required inside if; otherwise the condition is true whenever country is missing, which is rarely what you want.

Reusing definitions with $defs and $ref

{
  "$defs": {
    "address": {
      "type": "object",
      "properties": { "city": { "type": "string" } },
      "required": ["city"]
    }
  },
  "properties": {
    "billing": { "$ref": "#/$defs/address" },
    "shipping": { "$ref": "#/$defs/address" }
  }
}

Combine schemas with allOf (must match all), anyOf (at least one) and oneOf (exactly one). oneOf errors are hard to read, so prefer anyOf unless exclusivity matters.

Practical tips

Try it: JSON Schema Generator & Validator · JSON Validator. Free, no sign-up.

More articles