Validation 15 min read

JSON Schema: The Ultimate Validation Guide

Learn to validate JSON data with JSON Schema. From basic types to complex nested structures — with practical examples and real-world patterns.

#schema #validation #advanced #data-quality

TL;DR

  • JSON Schema = A contract for your JSON data
  • Validates types, structure, formats, and constraints
  • Catches bugs before they reach production
  • Provides automatic documentation for your APIs
  • Major validators: Ajv (JS), jsonschema (Python)
Why this matters: In my experience managing APIs that process millions of requests daily, JSON Schema validation catches 40% of bugs before they ever hit production. It's not just about validation—it's about creating a contract that makes your API self-documenting and your team more productive.

What is JSON Schema?

Imagine you're building an API. Users send you JSON data. But how do you know the data is correct? That the email field is actually an email? That age is a positive number?

You could write a bunch of if-statements. Or you could use JSON Schema — a standardized way to describe and validate JSON data.

Think of it like a blueprint. The schema says "this is what valid data looks like." The validator checks if incoming data matches that blueprint.

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

This schema says: "I expect an object with a string name, a non-negative integer age, and a properly formatted email. Name and email are required."

Why Use JSON Schema?

1. Catch Bugs Early

Invalid data gets rejected immediately, with clear error messages. No more debugging why your app crashed three layers deep.

2. Self-Documenting APIs

Your schema IS your documentation. Tools can generate API docs directly from schemas. OpenAPI/Swagger uses JSON Schema for exactly this.

3. Code Generation

Generate TypeScript types, form validation, and database models from a single schema. Write once, use everywhere.

4. Client-Side Validation

Validate user input in the browser before sending to the server. Better UX, fewer wasted API calls.

Basic Types

JSON Schema supports all JSON data types:

basic-types.json
json
{
  "stringField": { "type": "string" },
  "numberField": { "type": "number" },
  "integerField": { "type": "integer" },
  "booleanField": { "type": "boolean" },
  "nullField": { "type": "null" },
  "arrayField": { "type": "array" },
  "objectField": { "type": "object" }
}

Multiple Types

A field can accept multiple types:

multiple-types.json
json
{
  "id": { "type": ["string", "integer"] },
  "middleName": { "type": ["string", "null"] }
}

String Validation

Strings have powerful validation options:

string-validation.json
json
{
  "type": "object",
  "properties": {
    "username": {
      "type": "string",
      "minLength": 3,
      "maxLength": 20,
      "pattern": "^[a-zA-Z0-9_]+$"
    },
    "email": {
      "type": "string",
      "format": "email"
    },
    "website": {
      "type": "string",
      "format": "uri"
    },
    "birthDate": {
      "type": "string",
      "format": "date"
    },
    "bio": {
      "type": "string",
      "minLength": 10,
      "maxLength": 500
    }
  }
}

Built-in Formats

Format Description Example
email Email address user@example.com
uri Full URI https://example.com/path
date ISO 8601 date 2024-11-25
date-time ISO 8601 datetime 2024-11-25T12:00:00Z
uuid UUID 550e8400-e29b-41d4-...
ipv4 IPv4 address 192.168.1.1
ipv6 IPv6 address ::1

Number Validation

number-validation.json
json
{
  "type": "object",
  "properties": {
    "age": {
      "type": "integer",
      "minimum": 0,
      "maximum": 120
    },
    "price": {
      "type": "number",
      "minimum": 0,
      "exclusiveMinimum": 0,
      "multipleOf": 0.01
    },
    "quantity": {
      "type": "integer",
      "minimum": 1,
      "maximum": 100
    },
    "rating": {
      "type": "number",
      "minimum": 0,
      "maximum": 5,
      "multipleOf": 0.5
    }
  }
}
Note: exclusiveMinimum and exclusiveMaximum exclude the boundary value. So "minimum": 0, "exclusiveMinimum": true means "greater than 0" (not including 0).

Array Validation

array-validation.json
json
{
  "type": "object",
  "properties": {
    "tags": {
      "type": "array",
      "items": { "type": "string" },
      "minItems": 1,
      "maxItems": 10,
      "uniqueItems": true
    },
    "scores": {
      "type": "array",
      "items": {
        "type": "integer",
        "minimum": 0,
        "maximum": 100
      }
    },
    "matrix": {
      "type": "array",
      "items": {
        "type": "array",
        "items": { "type": "number" }
      }
    }
  }
}

Tuple Validation

For fixed-length arrays with specific types at each position:

tuple.json
json
{
  "coordinates": {
    "type": "array",
    "prefixItems": [
      { "type": "number" },
      { "type": "number" }
    ],
    "minItems": 2,
    "maxItems": 2
  }
}

Object Validation

object-validation.json
json
{
  "type": "object",
  "properties": {
    "name": { "type": "string" },
    "email": { "type": "string", "format": "email" }
  },
  "required": ["name", "email"],
  "additionalProperties": false
}

Key Options Explained

  • properties — Define the expected properties and their schemas
  • required — Array of property names that must be present
  • additionalProperties — Allow or disallow extra properties (false = strict)
  • minProperties / maxProperties — Limit number of properties

Pattern Properties

Validate properties by pattern:

pattern-props.json
json
{
  "type": "object",
  "patternProperties": {
    "^S_": { "type": "string" },
    "^N_": { "type": "number" }
  },
  "additionalProperties": false
}

This accepts {"S_name": "Alice", "N_age": 25} but rejects properties that don't match either pattern.

Nested Objects

Real-world data is nested. Schemas handle this elegantly:

nested-schema.json
json
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "user": {
      "type": "object",
      "properties": {
        "id": { "type": "integer" },
        "profile": {
          "type": "object",
          "properties": {
            "name": { "type": "string" },
            "avatar": { "type": "string", "format": "uri" }
          },
          "required": ["name"]
        },
        "settings": {
          "type": "object",
          "properties": {
            "theme": { "enum": ["light", "dark", "system"] },
            "notifications": { "type": "boolean" }
          }
        }
      },
      "required": ["id", "profile"]
    }
  },
  "required": ["user"]
}

Enums and Constants

Enum (One of Many)

enum.json
json
{
  "status": {
    "type": "string",
    "enum": ["pending", "active", "completed", "cancelled"]
  },
  "priority": {
    "type": "integer",
    "enum": [1, 2, 3, 4, 5]
  }
}

Const (Exact Value)

const.json
json
{
  "type": "object",
  "properties": {
    "version": { "const": "1.0.0" },
    "type": { "const": "user" }
  }
}

Combining Schemas

allOf — Must Match All

allof.json
json
{
  "allOf": [
    { "type": "object", "properties": { "name": { "type": "string" } } },
    { "type": "object", "properties": { "age": { "type": "integer" } } }
  ]
}

anyOf — Match At Least One

anyof.json
json
{
  "anyOf": [
    { "type": "string" },
    { "type": "integer" }
  ]
}

oneOf — Match Exactly One

oneof.json
json
{
  "oneOf": [
    { "type": "object", "properties": { "type": { "const": "email" }, "address": { "type": "string" } } },
    { "type": "object", "properties": { "type": { "const": "phone" }, "number": { "type": "string" } } }
  ]
}

Reusable Definitions ($defs)

Don't repeat yourself. Define schemas once, reference everywhere:

definitions.json
json
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$defs": {
    "address": {
      "type": "object",
      "properties": {
        "street": { "type": "string" },
        "city": { "type": "string" },
        "zipCode": { "type": "string", "pattern": "^[0-9]{5}$" }
      },
      "required": ["street", "city", "zipCode"]
    }
  },
  "type": "object",
  "properties": {
    "billingAddress": { "$ref": "#/$defs/address" },
    "shippingAddress": { "$ref": "#/$defs/address" }
  }
}

Conditional Validation

Different rules based on field values:

conditional.json
json
{
  "type": "object",
  "properties": {
    "paymentMethod": { "enum": ["credit", "bank", "crypto"] }
  },
  "if": {
    "properties": { "paymentMethod": { "const": "credit" } }
  },
  "then": {
    "properties": {
      "cardNumber": { "type": "string", "pattern": "^[0-9]{16}$" }
    },
    "required": ["cardNumber"]
  },
  "else": {
    "if": {
      "properties": { "paymentMethod": { "const": "bank" } }
    },
    "then": {
      "properties": {
        "accountNumber": { "type": "string" }
      },
      "required": ["accountNumber"]
    }
  }
}

Validating in JavaScript

The most popular JavaScript validator is Ajv. It's fast, compliant, and well-maintained:

ajv-example.js
javascript
import Ajv from 'ajv';
import addFormats from 'ajv-formats';

// Create validator instance
const ajv = new Ajv({ allErrors: true });
addFormats(ajv);

// Define schema
const schema = {
  type: 'object',
  properties: {
    name: { type: 'string', minLength: 1 },
    email: { type: 'string', format: 'email' },
    age: { type: 'integer', minimum: 0 }
  },
  required: ['name', 'email'],
  additionalProperties: false
};

// Compile schema
const validate = ajv.compile(schema);

// Validate data
const data = {
  name: 'Alice',
  email: 'alice@example.com',
  age: 25
};

if (validate(data)) {
  console.log('Valid!');
} else {
  console.log('Invalid:', validate.errors);
}

Error Messages

error-handling.js
javascript
const invalidData = {
  name: '',
  email: 'not-an-email',
  age: -5,
  extra: 'field'
};

if (!validate(invalidData)) {
  validate.errors.forEach(error => {
    console.log(`${error.instancePath} ${error.message}`);
  });
}
// Output:
// /name must NOT have fewer than 1 characters
// /email must match format "email"
// /age must be >= 0
// must NOT have additional properties
Try it live! Use our JSON Schema validator to test your schemas against data in real-time.

Real-World Schema Example

Here's a complete API request schema:

api-schema.json
json
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "title": "Create User Request",
  "description": "Schema for user registration API",
  "$defs": {
    "email": {
      "type": "string",
      "format": "email",
      "maxLength": 255
    }
  },
  "type": "object",
  "properties": {
    "username": {
      "type": "string",
      "minLength": 3,
      "maxLength": 30,
      "pattern": "^[a-zA-Z0-9_]+$",
      "description": "Alphanumeric username"
    },
    "email": { "$ref": "#/$defs/email" },
    "password": {
      "type": "string",
      "minLength": 8,
      "maxLength": 128,
      "description": "Must be at least 8 characters"
    },
    "profile": {
      "type": "object",
      "properties": {
        "firstName": { "type": "string", "maxLength": 50 },
        "lastName": { "type": "string", "maxLength": 50 },
        "bio": { "type": "string", "maxLength": 500 }
      }
    },
    "settings": {
      "type": "object",
      "properties": {
        "newsletter": { "type": "boolean", "default": false },
        "theme": { "enum": ["light", "dark"], "default": "light" }
      },
      "additionalProperties": false
    }
  },
  "required": ["username", "email", "password"],
  "additionalProperties": false
}

Best Practices

  • Start strict — Use additionalProperties: false to catch typos
  • Add descriptions — Document what each field is for
  • Use $defs — Reuse common patterns like email, phone, address
  • Validate on both sides — Client for UX, server for security
  • Version your schemas — Breaking changes need careful handling
  • Test your schemas — Write tests with valid and invalid data

Frequently Asked Questions

Which draft version should I use?

Use Draft 2020-12 for new projects. It's the latest stable version. Draft-07 is also widely supported if you need compatibility.

Can I generate TypeScript types from JSON Schema?

Yes! Use json-schema-to-typescript. It converts schemas to TypeScript interfaces automatically.

How do I validate custom formats?

With Ajv, use ajv.addFormat() to define custom validation functions for formats like phone numbers or custom IDs.

Continue Learning

About the Author

AT

Adam Tse

Founder & Lead Developer · 10+ years experience

Full-stack engineer with 10+ years of experience building developer tools and APIs. Previously worked on data infrastructure at scale, processing billions of JSON documents daily. Passionate about creating privacy-first tools that don't compromise on functionality.

JavaScript/TypeScript Web Performance Developer Tools Data Processing