UseToolSuite UseToolSuite

JSON Schema Validation: A Practical Guide

Learn how to validate JSON data structures using JSON Schema. Covers required fields, type checking, nested objects, conditional schemas, API integration with Ajv and jsonschema, and best practices for production use.

Necmeddin Cunedioglu Necmeddin Cunedioglu 8 min read

Practice what you learn

JSON Formatter & Validator

Try it free →

JSON Schema Validation: A Practical Guide

JSON Schema is a declarative language for validating the structure, content, and format of JSON data. If you build APIs or process JSON from external sources, schema validation prevents malformed data from causing bugs downstream — catching type mismatches, missing required fields, and constraint violations at the boundary before they propagate through your application.

This guide covers the core JSON Schema vocabulary (Draft 2020-12), advanced patterns like conditional validation and schema composition, integration with popular frameworks, and production best practices for API validation. Every example is tested and ready to copy into your project.

Why Validate JSON?

JSON.parse() only checks syntax — it confirms that the string is valid JSON. But it does not verify that the data contains the fields you expect, with the types you need:

// Valid JSON, but is it valid for YOUR application?
const data = JSON.parse('{"name": 123, "email": null}');
// name should be a string, email should be a valid address
// JSON.parse() says this is fine — your business logic crashes later

Without schema validation, you discover data problems deep in your application — in a database write, a template render, or worse, in production. Schema validation catches these issues at the entry point, producing clear, actionable error messages.

The cost of not validating

ScenarioWithout SchemaWith Schema
Missing required fieldTypeError: Cannot read property 'name' of undefined at line 847"required property 'name' is missing" at the API boundary
Wrong typeSilent data corruption in database"'age' must be integer, got string" before database write
Invalid enum valueUnexpected UI state, user confusion"'status' must be one of: active, inactive, banned"
Extra/unknown fieldsStored in DB, wasting space, potential injectionRejected immediately with additionalProperties: false

JSON Schema solves this by letting you define expectations:

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "required": ["name", "email"],
  "properties": {
    "name": { "type": "string", "minLength": 1 },
    "email": { "type": "string", "format": "email" }
  }
}

Validate your JSON first — Before testing schemas, ensure your JSON is syntactically valid with our JSON Formatter.

Core Keywords

Type Checking

JSON Schema supports seven primitive types:

{ "type": "string" }
{ "type": "number", "minimum": 0, "maximum": 100 }
{ "type": "integer" }
{ "type": "boolean" }
{ "type": "null" }
{ "type": "array", "items": { "type": "string" }, "minItems": 1 }
{ "type": "object" }

You can also accept multiple types:

{ "type": ["string", "null"] }

This is useful for nullable fields — the value must be either a string or null.

String Constraints

{
  "type": "string",
  "minLength": 1,
  "maxLength": 255,
  "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$",
  "format": "email"
}

Built-in format values include: email, uri, date, date-time, ipv4, ipv6, uuid, hostname, and regex. Note that format validation is optional in Ajv — you must install ajv-formats to enable it.

Test regex patterns used in your schemas with our Regex Tester before deploying.

Number Constraints

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

multipleOf: 0.01 is useful for currency values — it ensures at most 2 decimal places.

Required Fields

{
  "type": "object",
  "required": ["id", "name"],
  "properties": {
    "id": { "type": "integer" },
    "name": { "type": "string" },
    "bio": { "type": "string" }
  }
}

Here id and name are mandatory; bio is optional. Any request missing id or name will fail validation with a clear error message.

Nested Objects

{
  "type": "object",
  "properties": {
    "address": {
      "type": "object",
      "required": ["city", "country"],
      "properties": {
        "street": { "type": "string" },
        "city": { "type": "string" },
        "country": { "type": "string", "minLength": 2, "maxLength": 2 }
      },
      "additionalProperties": false
    }
  }
}

Arrays with Constraints

{
  "type": "array",
  "items": {
    "type": "object",
    "required": ["name", "price"],
    "properties": {
      "name": { "type": "string" },
      "price": { "type": "number", "minimum": 0 },
      "tags": {
        "type": "array",
        "items": { "type": "string" },
        "uniqueItems": true,
        "maxItems": 10
      }
    }
  },
  "minItems": 1,
  "maxItems": 100
}

uniqueItems: true ensures no duplicate values in the array.

Enums and Constants

{
  "properties": {
    "status": { "type": "string", "enum": ["active", "inactive", "banned"] },
    "version": { "const": 2 },
    "zipCode": { "type": "string", "pattern": "^[0-9]{5}(-[0-9]{4})?$" }
  }
}

const validates that the value is exactly the specified value — useful for API versioning.

Advanced Patterns

Conditional Validation (if/then/else)

Validate different fields based on the value of another field:

{
  "type": "object",
  "properties": {
    "paymentMethod": { "type": "string", "enum": ["credit_card", "bank_transfer", "paypal"] }
  },
  "if": {
    "properties": { "paymentMethod": { "const": "credit_card" } }
  },
  "then": {
    "required": ["cardNumber", "expiry", "cvv"],
    "properties": {
      "cardNumber": { "type": "string", "pattern": "^[0-9]{16}$" },
      "expiry": { "type": "string", "pattern": "^(0[1-9]|1[0-2])/[0-9]{2}$" },
      "cvv": { "type": "string", "pattern": "^[0-9]{3,4}$" }
    }
  },
  "else": {
    "if": {
      "properties": { "paymentMethod": { "const": "bank_transfer" } }
    },
    "then": {
      "required": ["iban"],
      "properties": {
        "iban": { "type": "string", "pattern": "^[A-Z]{2}[0-9]{2}[A-Z0-9]{4,30}$" }
      }
    }
  }
}

Schema Composition (allOf, oneOf, anyOf)

Combine multiple schemas:

{
  "allOf": [
    { "$ref": "#/$defs/baseUser" },
    { "$ref": "#/$defs/address" }
  ],
  "$defs": {
    "baseUser": {
      "type": "object",
      "required": ["name", "email"],
      "properties": {
        "name": { "type": "string" },
        "email": { "type": "string", "format": "email" }
      }
    },
    "address": {
      "type": "object",
      "properties": {
        "city": { "type": "string" },
        "country": { "type": "string" }
      }
    }
  }
}
KeywordBehavior
allOfData must match ALL schemas (intersection)
oneOfData must match EXACTLY ONE schema (exclusive or)
anyOfData must match AT LEAST ONE schema (inclusive or)
notData must NOT match the schema

Reusable Definitions ($ref and $defs)

{
  "$defs": {
    "address": {
      "type": "object",
      "required": ["street", "city"],
      "properties": {
        "street": { "type": "string" },
        "city": { "type": "string" },
        "zip": { "type": "string" }
      }
    }
  },
  "type": "object",
  "properties": {
    "billingAddress": { "$ref": "#/$defs/address" },
    "shippingAddress": { "$ref": "#/$defs/address" }
  }
}

$ref eliminates duplication — define once, reference everywhere.

Integrating JSON Schema in Your Workflow

Node.js — Ajv (fastest JSON Schema validator)

import Ajv from 'ajv';
import addFormats from 'ajv-formats';

const ajv = new Ajv({ allErrors: true }); // Report all errors, not just the first
addFormats(ajv); // Enable format validation (email, uri, date, etc.)

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

const validate = ajv.compile(schema);

function validateUser(data) {
  const valid = validate(data);
  if (!valid) {
    return {
      success: false,
      errors: validate.errors.map(e => ({
        field: e.instancePath || e.params?.missingProperty,
        message: e.message,
      })),
    };
  }
  return { success: true, data };
}

// Usage
const result = validateUser({ name: '', email: 'invalid', age: -5 });
// → { success: false, errors: [
//   { field: '/name', message: 'must NOT have fewer than 1 characters' },
//   { field: '/email', message: 'must match format "email"' },
//   { field: '/age', message: 'must be >= 0' }
// ]}

Express.js middleware

function validateBody(schema) {
  const validate = ajv.compile(schema);
  return (req, res, next) => {
    if (!validate(req.body)) {
      return res.status(400).json({
        error: 'Validation failed',
        details: validate.errors,
      });
    }
    next();
  };
}

app.post('/api/users', validateBody(userSchema), (req, res) => {
  // req.body is guaranteed to match the schema
});

Python — jsonschema

from jsonschema import validate, ValidationError, Draft202012Validator

schema = {
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "type": "object",
    "required": ["name", "email"],
    "properties": {
        "name": {"type": "string", "minLength": 1},
        "email": {"type": "string", "format": "email"},
    },
    "additionalProperties": False,
}

# Simple validation
try:
    validate(instance=data, schema=schema)
except ValidationError as e:
    print(e.message)

# Collect ALL errors (not just the first)
validator = Draft202012Validator(schema)
errors = list(validator.iter_errors(data))
for error in errors:
    print(f"{error.json_path}: {error.message}")

FastAPI (Python) — automatic schema validation

from pydantic import BaseModel, EmailStr
from fastapi import FastAPI

class User(BaseModel):
    name: str
    email: EmailStr
    age: int = Field(ge=0, le=150)

app = FastAPI()

@app.post("/users")
def create_user(user: User):  # Pydantic validates automatically
    return {"id": 1, **user.dict()}

FastAPI + Pydantic generates JSON Schema automatically from Python type hints and validates every request.

From JSON Schema to TypeScript

Once you define a JSON Schema, you can generate TypeScript interfaces automatically. This creates a single source of truth for both runtime validation (JSON Schema) and compile-time type checking (TypeScript):

npx json-schema-to-typescript schema.json -o types.ts

Try it: Convert your JSON data to TypeScript interfaces instantly with our JSON to TypeScript Converter.

Best Practices

  1. Start with required — Explicitly list required fields; do not rely on consumers sending all fields.
  2. Use additionalProperties: false — Reject unexpected fields for strict validation. This prevents typos (emal instead of email) from silently passing.
  3. Version your schemas — Include a $id with version number ("$id": "https://api.example.com/schemas/user/v2") for API evolution.
  4. Validate early — Run schema validation at the API boundary, not deep in business logic. Fail fast with clear error messages.
  5. Use allErrors: true — Return all validation errors, not just the first. This reduces round-trips for the API consumer.
  6. Document with title and description — Add human-readable descriptions to your schemas for auto-generated documentation.
  7. Test your schemas — Write unit tests that verify your schema accepts valid data AND rejects invalid data.
{
  "$id": "https://api.example.com/schemas/user/v2",
  "title": "User",
  "description": "A registered user in the system",
  "type": "object",
  "required": ["name", "email"],
  "properties": {
    "name": {
      "type": "string",
      "title": "Full Name",
      "description": "User's display name",
      "minLength": 1,
      "maxLength": 100
    }
  }
}

Frequently Asked Questions

What JSON Schema draft should I use?

Use Draft 2020-12 (the latest) for new projects. It has the best feature set including $dynamicRef, prefixItems, and cleaner vocabulary definitions. If you are using Ajv, note that Ajv v8 supports Draft 2020-12 with the ajv/dist/2020 import.

How does JSON Schema compare to TypeScript types?

JSON Schema validates data at runtime (API boundaries, file imports, user input). TypeScript validates at compile time (development, type checking). They are complementary: TypeScript catches type errors during development, JSON Schema catches them in production when receiving data from external sources. Use both for maximum safety.

Can JSON Schema validate business logic?

JSON Schema validates structure and format, not business logic. For example, it can ensure startDate is a valid date string, but it cannot validate that startDate is before endDate (this requires application code). Use JSON Schema for structural validation, then apply business rules in your application layer.

Is JSON Schema validation slow?

No. Ajv compiles schemas into optimized JavaScript functions, making validation extremely fast — typically under 1ms for a moderately complex schema. For comparison, Ajv can validate over 100,000 objects per second. Schema compilation happens once; validation runs are nearly free.


This article is part of our JSON Developer Guide series.

Necmeddin Cunedioglu
Necmeddin Cunedioglu Author

Software developer and the creator of UseToolSuite. I write about the tools and techniques I use daily as a developer — practical guides based on real experience, not theory.