HandyTools Hub

← All guides

JSON Schema Explained: Validation, Types, and the Keywords That Do the Real Work

2026-08-24

JSON is the most tolerant data format in the world — and that’s its weakness. An API accepts {"user_id": 123}, and some caller sends {"user_id": "123"} or {"userId": 123} or {}. Nothing complains until a crash later. JSON Schema is the contract that prevents that. It’s a JSON document that describes another JSON document: what type each field must be, which fields are required, what strings and numbers may look like, and how arrays and nested objects are shaped. This guide walks through the handful of keywords that cover 90% of real validation.

A Schema Is a JSON Document

A schema is ordinary JSON with a special $schema marker and a type:

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "type": "object",
  "properties": {
    "name": { "type": "string" },
    "age": { "type": "integer" }
  },
  "required": ["name"]
}

Read it as a set of constraints, not a recipe: “the value must be an object; its name must be a string and is required; its age, if present, must be an integer.” Any data that satisfies the constraints validates; extra data is allowed unless you forbid it (see additionalProperties below).

The Type System

The type keyword accepts: object, array, string, number, integer, boolean, null. A value that doesn’t match the declared type fails validation outright.

{ "type": "string" }      // "hello" ✓  |  42 ✗
{ "type": ["string", "null"] }  // either is fine

The ["string", "null"] array form is the idiomatic “nullable” — a field that’s null or a string. (Draft 2020-12 added type: "null" and moved type arrays elsewhere, but the array form still works everywhere.)

Objects: properties, required, additionalProperties

Three keywords define object validation:

  • properties — a map of field name → schema. Each declared field’s value is validated against its schema.
  • required — an array of field names that must be present. Absent ≠ invalid by default; only listed fields are mandatory.
  • additionalProperties — what happens to fields not in properties. Default: allowed. false forbids them (strict contracts); a schema validates them loosely.
{
  "type": "object",
  "properties": {
    "id": { "type": "integer" },
    "tags": { "type": "array" }
  },
  "required": ["id"],
  "additionalProperties": false
}

additionalProperties: false is the strictness switch that catches typos like user_id vs userId at the boundary instead of downstream.

Strings and Numbers

Strings get length and pattern constraints; numbers get range and multiple-of:

{
  "type": "string",
  "minLength": 3,
  "maxLength": 50,
  "pattern": "^[a-z0-9-]+$"
}
{
  "type": "number",
  "minimum": 0,
  "exclusiveMinimum": 0,
  "maximum": 100,
  "multipleOf": 0.5
}

exclusiveMinimum / exclusiveMaximum (when boolean in draft-04, a separate keyword from draft-06 on) express strict inequalities. pattern is a full regex match, not a “contains” search.

Arrays

Arrays validate each element, plus size and uniqueness:

{
  "type": "array",
  "items": { "type": "string" },
  "minItems": 1,
  "maxItems": 10,
  "uniqueItems": true
}

items as a single schema validates every element the same way. As an array (prefixItems in 2020-12) it’s a tuple: element 0 against schema 0, element 1 against schema 1, and so on — the right shape for fixed-position data like coordinate pairs.

Composition: oneOf, allOf, anyOf, and $ref

Real-world contracts need branching logic:

  • oneOf — exactly one subschema must match: “either an email address or a phone number.”
  • anyOf — at least one must match (useful for “string or array of strings”).
  • allOf — all must match (combining base constraints with extras).
  • $ref — points at a schema elsewhere in the same document ("$ref": "#/$defs/address") or in another document. This is how you reuse a shape instead of repeating it:
{
  "type": "object",
  "properties": {
    "billing": { "$ref": "#/$defs/address" },
    "shipping": { "$ref": "#/$defs/address" }
  },
  "$defs": {
    "address": {
      "type": "object",
      "required": ["street", "city"],
      "additionalProperties": false
    }
  }
}

One caution: $ref is exclusive in draft-07 (it replaces sibling keywords), so put shared logic in $defs and reference it rather than mixing $ref with properties on the same node.

What Schemas Are Actually For

Validation is only the beginning. A good schema is a source of truth that powers:

  • API request/response validation — reject bad payloads at the boundary with precise error messages.
  • Generated forms — a schema-driven form renderer builds fields, types, and required-ness straight from the contract, so the form and the API can’t drift apart.
  • Test data generation — feed the schema to a generator and get realistic, valid fixtures for every branch of the contract.
  • Documentation — the schema is the API spec for the shape of your data.

The same document serves all four, which is why keeping the schema strict pays off beyond validation.

Quick Reference

  • A schema is a JSON document of constraints; $schema declares the dialect, type declares the outer shape.
  • Types: object, array, string, number, integer, boolean, null; ["string","null"] for nullable.
  • Objects: properties (per-field schemas), required (must-be-present list), additionalProperties: false (strictness).
  • Strings: minLength, maxLength, pattern (full match). Numbers: minimum, exclusiveMinimum, maximum, multipleOf.
  • Arrays: items (per-element) or tuple via array form; minItems, maxItems, uniqueItems.
  • Branching: oneOf (exactly one), anyOf (at least one), allOf (all), and $ref to reuse a shape from $defs.
  • Draft-07 $ref replaces sibling keywords on the same node — reference into $defs instead of mixing.
  • A schema drives validation, generated forms, test data, and docs from one source of truth.

To build a schema for your JSON instead of typing it by hand, the JSON Schema Generator derives the shape from a sample or builds it visually, the Test Data Generator turns that schema into realistic fixtures, and the JSON Formatter keeps your source data readable while you design the contract. Schemas sit alongside the other JSON headaches covered in common JSON errors and the configuration pitfall in YAML — all three are about turning forgiving text into dependable data.