JSON Schema Explained: Validate JSON Like a Pro

Published 2026-04-12 · 10 min read

JSON is loose by design — any parser will happily accept { "age": "thirty" }, even though your application expects a number. JSON Schema is the standard way to describe the shape your data is supposed to have and to validate real-world JSON against that description. This guide walks through the fundamentals with runnable examples.

Why bother with a schema?

Without a schema, every service in your architecture independently decides what counts as "a valid user object". With a schema, you have one canonical definition that the producer and the consumer can both reference. That gives you:

  • Automated validation at API boundaries.
  • Generated documentation.
  • Auto-generated types in TypeScript, Go, Rust, and others.
  • Client SDKs that know what fields exist.

A minimal schema

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

This schema says: the document is an object, it has name (a required string) and age (an optional non-negative integer). A validator running against { "name": "Ada", "age": 206 } would return valid; { "age": -1 } would return two errors — name missing and age below minimum.

The type system

type accepts any of: string, number, integer, boolean, null, array, object. You can also pass an array of types to allow multiple: { "type": ["string", "null"] }.

Constraining strings

{
  "type": "string",
  "minLength": 3,
  "maxLength": 50,
  "pattern": "^[a-z0-9_-]+$",
  "format": "email"
}

format is a hint — validators may or may not enforce specific formats like email, uri, date-time, or uuid. Most production validators enforce the common ones.

Constraining numbers

{
  "type": "number",
  "minimum": 0,
  "exclusiveMaximum": 100,
  "multipleOf": 0.01
}

Arrays

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

Arrays get a items schema (applied to every element) plus length and uniqueness constraints. For tuples with positional types, use prefixItems.

Composition: allOf, anyOf, oneOf

Real schemas combine simpler ones. allOf requires data to match all listed schemas (intersection), anyOf requires at least one (union), oneOf requires exactly one (discriminated union). These are how you model inheritance and polymorphism.

{
  "oneOf": [
    { "type": "object", "properties": { "type": { "const": "text" }, "body": { "type": "string" } } },
    { "type": "object", "properties": { "type": { "const": "image" }, "url":  { "type": "string" } } }
  ]
}

References with $ref

Break large schemas into reusable pieces using $ref. Define shared shapes in $defs and reference them with #/$defs/Name.

{
  "$defs": {
    "address": {
      "type": "object",
      "properties": { "city": { "type": "string" } }
    }
  },
  "type": "object",
  "properties": {
    "home":    { "$ref": "#/$defs/address" },
    "billing": { "$ref": "#/$defs/address" }
  }
}

Running a validator

In JavaScript, Ajv is the standard. In Python, jsonschema. Most languages have at least one mature library. Load the schema once, compile it, and validate many documents against the compiled version — compilation is expensive, validation is cheap.

Start small

Don't try to model every edge case on day one. Start with the required fields, add constraints where misuse actually causes bugs, and grow the schema as you learn. Paste your data into the JSON Formatter first to understand its shape, then write a schema that matches.

More from the blog