BytePane

JSON Schema Validator: Validate JSON Against a Schema

JSON17 min read

The Myth: TypeScript Already Validates Your JSON

This is the most common mistake in typed JavaScript codebases. TypeScript's static type system is erased at runtime — the compiled JavaScript has no memory of your interfaces or type annotations. When you write const user: User = JSON.parse(response.body), TypeScript trusts you that the parsed JSON matches the User type. It does not verify. If the API sends "age": "thirty" instead of "age": 30, your code gets a string where it expects a number — with no compile-time or runtime error.

// TypeScript does NOT protect you here:
interface User {
  id: number;
  email: string;
  age: number;
}

const responseBody = '{"id": "not-a-number", "email": 42, "age": "thirty"}';
const user: User = JSON.parse(responseBody);  // ✓ TypeScript is happy
//                                                 ✗ Runtime: wrong types everywhere

// A JSON Schema validator would catch this immediately:
// Error: /id must be integer (received "not-a-number")
// Error: /email must be string (received 42)
// Error: /age must be integer (received "thirty")

Runtime JSON Schema validation fills the gap that TypeScript's compile-time system cannot fill. Together, they are complementary: TypeScript prevents type errors you introduce yourself in code; a JSON Schema validator catches contract violations from external systems.

Key Takeaways

  • AJV has 256M weekly npm downloads (2026) — it compiles schemas to JIT-optimized functions, validating at 3–5M ops/sec
  • TypeScript types are erased at runtime — they do not validate JSON from external sources. You need a runtime validator
  • JSON Schema draft 2020-12 is the current stable spec — use import Ajv2020 from 'ajv/dist/2020' to target it
  • Use additionalProperties: false for request schemas, never for response schemas (breaks forward compatibility)
  • Always compile AJV schemas once at startup — compiling per-request adds ~1–5ms overhead, negating AJV's speed advantage

JSON Schema in 60 Seconds

JSON Schema is a vocabulary for describing the structure of JSON data. A schema is itself a JSON document that specifies what valid instances must look like: allowed types, required properties, value constraints, string patterns. The current stable specification is draft 2020-12, maintained by the JSON Schema organization.

// A minimal JSON Schema for a user object:
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "id":    { "type": "integer" },
    "email": { "type": "string", "format": "email" },
    "role":  { "type": "string", "enum": ["admin", "editor", "viewer"] },
    "age":   { "type": "integer", "minimum": 13, "maximum": 120 }
  },
  "required": ["id", "email", "role"],
  "additionalProperties": false
}

// Valid instance — passes:
{ "id": 42, "email": "[email protected]", "role": "admin" }

// Invalid instances — fail:
{ "id": "not-a-number", "email": "[email protected]", "role": "admin" }
// Error: /id must be integer

{ "id": 42, "email": "[email protected]" }
// Error: must have required property 'role'

{ "id": 42, "email": "[email protected]", "role": "superuser" }
// Error: /role must be equal to one of the allowed values

JSON Schema is more pervasive than most developers realize. OpenAPI 3.x uses JSON Schema for all request and response body definitions. VS Code's IntelliSense for JSON files (tsconfig.json, package.json, .eslintrc) runs against JSON Schema definitions from the SchemaStore catalog, which hosts over 700 schemas as of 2026. Kubernetes manifest validation uses JSON Schema. If you have ever seen IntelliSense suggestions inside a .json file in VS Code, you were benefiting from JSON Schema validation in real time.

Validators by Ecosystem: Which to Use

ValidatorLanguagePerformanceTS SupportBest For
AJVNode.js / JS~3–5M ops/secManual type guardsHigh-throughput APIs, OpenAPI
ZodTypeScript~300–500K ops/secFirst-class (z.infer)TypeScript-first apps, tRPC, Remix
ValibotTypeScript~1M ops/secFirst-classBundle-size sensitive (1.2KB vs Zod's 57KB)
jsonschema (Python)PythonModerateN/APython scripts, all drafts supported
Pydantic v2PythonFast (Rust core)First-classFastAPI, Django REST, Python APIs
jsonschema-rsRust + Python bindingsVery fastN/AHigh-throughput Python validation
gojsonschemaGoGoodN/AGo APIs, draft-07 compatible

AJV: Production Node.js Setup

// npm install ajv ajv-formats ajv-errors
import Ajv2020 from 'ajv/dist/2020'
import addFormats from 'ajv-formats'

// Initialize ONCE at module load — compilation is expensive
const ajv = new Ajv2020({
  allErrors: true,        // Collect ALL errors, not just first
  // coerceTypes: false   // NEVER enable for API input
  // useDefaults: true    // Optionally fill in default values
})
addFormats(ajv)           // date-time, email, uri, uuid, ipv4, etc.

// Define and compile schema ONCE at startup
const ProductSchema = {
  $schema: 'https://json-schema.org/draft/2020-12/schema',
  type: 'object',
  properties: {
    id:       { type: 'integer', minimum: 1 },
    name:     { type: 'string', minLength: 1, maxLength: 200 },
    price:    { type: 'number', minimum: 0, exclusiveMinimum: 0 },
    currency: { type: 'string', pattern: '^[A-Z]{3}$' },
    tags:     { type: 'array', items: { type: 'string' }, uniqueItems: true },
    sku:      { type: 'string', format: 'uuid' },
  },
  required: ['id', 'name', 'price', 'currency'],
  additionalProperties: false,
}

// Compile once — validate.errors is populated on failure
const validateProduct = ajv.compile(ProductSchema)

// TypeScript type assertion function
function assertProduct(data: unknown): asserts data is Product {
  if (!validateProduct(data)) {
    const errors = validateProduct.errors!
      .map(e => `${e.instancePath || '(root)'} ${e.message}`)
      .join('; ')
    throw new ValidationError(`Invalid product: ${errors}`)
  }
}

// Express route — validate at the boundary
app.post('/products', express.json(), (req, res) => {
  if (!validateProduct(req.body)) {
    return res.status(422).json({
      error: 'Validation failed',
      details: validateProduct.errors!.map(e => ({
        field: e.instancePath,
        message: e.message,
        value: e.data,
      })),
    })
  }
  // req.body is now structurally sound
  const product = req.body as Product
  return createProduct(product)
})

Zod: TypeScript-First Validation

// npm install zod
import { z } from 'zod'

// Define once — get TypeScript type AND runtime validator
const ProductSchema = z.object({
  id:       z.number().int().positive(),
  name:     z.string().min(1).max(200),
  price:    z.number().positive(),
  currency: z.string().regex(/^[A-Z]{3}$/),
  tags:     z.array(z.string()).optional(),
  sku:      z.string().uuid().optional(),
})

// Infer TypeScript type automatically — zero duplication:
type Product = z.infer<typeof ProductSchema>
// { id: number; name: string; price: number; currency: string; tags?: string[]; sku?: string }

// Parse (throws on invalid) vs safeParse (returns result object):
const result = ProductSchema.safeParse(req.body)
if (!result.success) {
  return res.status(422).json({
    error: 'Validation failed',
    details: result.error.flatten().fieldErrors,
  })
}
const product = result.data  // fully typed as Product

// Export as JSON Schema for OpenAPI documentation:
import { zodToJsonSchema } from 'zod-to-json-schema'
const jsonSchema = zodToJsonSchema(ProductSchema, 'Product')
// → valid draft 2020-12 JSON Schema document

Python: jsonschema and Pydantic

# pip install jsonschema pydantic

# Option 1: jsonschema (standard library approach, all drafts)
from jsonschema import validate, ValidationError, Draft202012Validator

product_schema = {
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "type": "object",
    "properties": {
        "id":       { "type": "integer", "minimum": 1 },
        "name":     { "type": "string", "minLength": 1 },
        "price":    { "type": "number", "exclusiveMinimum": 0 },
        "currency": { "type": "string", "pattern": "^[A-Z]{3}$" },
    },
    "required": ["id", "name", "price", "currency"],
    "additionalProperties": False,
}

# For single validation (raises on error):
try:
    validate(instance=data, schema=product_schema, cls=Draft202012Validator)
except ValidationError as e:
    print(f"Invalid: {e.message} at {e.json_path}")

# For collecting ALL errors (production use):
validator = Draft202012Validator(product_schema)
errors = list(validator.iter_errors(data))
if errors:
    raise ValueError([e.message for e in errors])

# Option 2: Pydantic v2 (Rust core, type inference, FastAPI integration)
from pydantic import BaseModel, Field, field_validator
from typing import Optional
import re

class Product(BaseModel):
    id: int = Field(gt=0)
    name: str = Field(min_length=1, max_length=200)
    price: float = Field(gt=0)
    currency: str
    tags: Optional[list[str]] = None

    @field_validator('currency')
    @classmethod
    def validate_currency(cls, v: str) -> str:
        if not re.match(r'^[A-Z]{3}$', v):
            raise ValueError('Must be 3 uppercase letters (ISO 4217)')
        return v

# Parse and validate:
try:
    product = Product.model_validate(req_body)  # raises ValidationError on failure
except ValidationError as e:
    return JSONResponse(status_code=422, content={"errors": e.errors()})

# Export JSON Schema for OpenAPI:
schema = Product.model_json_schema()  # → valid JSON Schema document

Online JSON Schema Validators

For quick schema iteration during development — testing that your schema correctly rejects invalid data, or verifying a third-party schema you've received — browser-based validators are faster than setting up a local project. Here is what to look for:

ToolDraft SupportError DisplayNotable Feature
BytePane JSON ValidatorDraft 2020-12, 07, 2019-09Path + message, inline highlightingClient-side AJV, no data sent to server
jsonschemavalidator.netDraft 07, 2019-09, 2020-12Error list with JSON PointerUses Newtonsoft.Json.Schema (.NET)
json-schema.org validatorAll official draftsStandard error outputReference implementation, official
VS Code (built-in)Via $schema propertyInline squiggles + hover tooltipsReal-time as you type; SchemaStore integration

The most underrated JSON Schema validator is VS Code itself. Add a $schema property to any JSON file pointing to a schema URL (or a local path), and VS Code validates in real time with inline error highlighting. The JSON Schema Store (schemastore.org) provides 700+ pre-built schemas for config files — VS Code automatically fetches and applies them for recognized filenames.

Draft 2020-12: What Changed and Why It Matters

If you are reading older tutorials, several keywords were renamed or redesigned in draft 2019-09 and finalized in 2020-12. The two most commonly hit migration issues:

// ❌ Draft 07: tuple validation (DEPRECATED)
{
  "type": "array",
  "items": [
    { "type": "string" },    // index 0: string
    { "type": "integer" }   // index 1: integer
  ],
  "additionalItems": false
}

// ✅ Draft 2020-12: tuple validation
{
  "type": "array",
  "prefixItems": [          // positional items → prefixItems
    { "type": "string" },
    { "type": "integer" }
  ],
  "items": false            // additionalItems → items for extra elements
}

// ❌ Draft 07: definitions (old name)
{
  "definitions": {
    "Address": { ... }
  },
  "properties": {
    "address": { "$ref": "#/definitions/Address" }
  }
}

// ✅ Draft 2020-12: $defs (new name)
{
  "$defs": {
    "Address": { ... }
  },
  "properties": {
    "address": { "$ref": "#/$defs/Address" }
  }
}

// unevaluatedProperties (2020-12 addition) vs additionalProperties:
// additionalProperties only considers properties defined in the SAME schema object
// unevaluatedProperties considers ALL properties evaluated anywhere in the schema
// → Use unevaluatedProperties: false when composing with allOf/anyOf/oneOf

// AJV 2020-12 import:
import Ajv2020 from 'ajv/dist/2020'       // for draft 2020-12
import Ajv from 'ajv'                       // for draft-07 (default)

Common Schema Patterns and Gotchas

Nullable Fields

// Field that can be a string OR null:
{
  "type": ["string", "null"]   // draft 2020-12 type arrays
}

// Optional field (can be omitted entirely, but if present must be a string):
// Solution: don't put it in "required" — absence is valid
// Add it to "properties" with the type constraint

// Optional AND nullable (can be absent OR null OR a string):
{
  "properties": {
    "middleName": { "type": ["string", "null"] }
  }
  // middleName NOT in "required" → absence is fine
  // type array → null is fine if present
  // type array → string is fine if present
}

Discriminated Unions (Polymorphic Payloads)

// Different payload shape based on a type discriminator field
{
  "oneOf": [
    {
      "type": "object",
      "properties": {
        "type":       { "const": "email" },
        "to":         { "type": "string", "format": "email" },
        "subject":    { "type": "string" }
      },
      "required": ["type", "to", "subject"],
      "additionalProperties": false
    },
    {
      "type": "object",
      "properties": {
        "type":        { "const": "sms" },
        "phoneNumber": { "type": "string", "pattern": "^\+[1-9]\d{1,14}$" }
      },
      "required": ["type", "phoneNumber"],
      "additionalProperties": false
    }
  ]
}

// Faster validation in AJV with discriminator keyword (OpenAPI extension):
const ajv = new Ajv({ discriminator: true })
// Add "discriminator": { "propertyName": "type" } to the schema
// AJV skips checking non-matching sub-schemas → faster + clearer errors

The additionalProperties Trap in allOf

// ❌ This does NOT work as expected with additionalProperties: false
{
  "allOf": [
    {
      "properties": { "id": { "type": "integer" } },
      "required": ["id"]
    },
    {
      "properties": { "email": { "type": "string" } },
      "required": ["email"],
      "additionalProperties": false   // Only sees "email" in its own schema!
      // → REJECTS objects with "id" because "id" is "additional" here
    }
  ]
}

// ✅ Correct: use unevaluatedProperties: false at the TOP level
{
  "allOf": [
    {
      "properties": { "id": { "type": "integer" } },
      "required": ["id"]
    },
    {
      "properties": { "email": { "type": "string" } },
      "required": ["email"]
      // No additionalProperties here
    }
  ],
  "unevaluatedProperties": false   // Sees ALL evaluated properties from allOf
  // → Accepts exactly { id, email }, rejects anything else
}

Where to Place Validation in Production Code

Validation overhead is real — even AJV at 3–5M ops/sec adds a small cost per request. Place validation strategically, not everywhere:

LocationValidate?Reasoning
Incoming API requestsAlwaysExternal boundary — trust nothing
App startup configAlwaysFail fast, not mid-request
Webhook ingestionAlwaysThird parties change contracts silently
Response from your own DBRarelyAlready validated on write; adds latency
Inter-service calls (other team)YesOther team's runtime ≠ your compile-time types
Internal function callsNoTypeScript handles this; schema overhead is pure waste

To inspect and understand JSON payloads before writing schemas, our JSON Formatter makes it easy to pretty-print and explore nested structures. For converting JSON to YAML (often used in Kubernetes and OpenAPI), see our JSON to YAML converter.

Generating JSON Schemas from Existing Code

For brownfield projects with existing TypeScript types, writing JSON Schemas from scratch is tedious and error-prone. Several tools automate this:

// Option 1: TypeScript → JSON Schema (ts-json-schema-generator)
// npm install -g ts-json-schema-generator
// ts-json-schema-generator --path src/types.ts --type User > user-schema.json

// Option 2: Zod → JSON Schema
import { z } from 'zod'
import { zodToJsonSchema } from 'zod-to-json-schema'

const UserSchema = z.object({
  id: z.number().int().positive(),
  email: z.string().email(),
  role: z.enum(['admin', 'editor', 'viewer']),
})

const schema = zodToJsonSchema(UserSchema, {
  name: 'User',
  target: 'jsonSchema2019-09',  // or 'jsonSchema2020-12'
})

// Option 3: Pydantic → JSON Schema (Python)
from pydantic import BaseModel
from typing import Literal

class User(BaseModel):
    id: int
    email: str
    role: Literal['admin', 'editor', 'viewer']

schema = User.model_json_schema()
# Outputs: valid JSON Schema document with all type constraints

# Option 4: Infer schema from sample data (quicktype)
# npx quicktype --lang schema --src sample.json > inferred-schema.json
# Useful for reverse-engineering undocumented APIs

Frequently Asked Questions

What is the fastest JSON Schema validator for Node.js?

AJV (Another JSON Schema Validator) with 256 million weekly npm downloads as of 2026. It compiles schemas to optimized JavaScript functions at startup, achieving 3–5 million validations per second. For TypeScript-first projects, Zod trades raw speed (~300–500K ops/sec) for first-class TypeScript inference — write a schema once, get both runtime validation and compile-time types.

Which JSON Schema draft should I use in 2026?

Draft 2020-12 for new projects — it is the current stable spec with broadest validator support. Key changes from draft-07: prefixItems replaces positional items arrays for tuple validation; unevaluatedProperties is preferred over additionalProperties for composed schemas; $defs replaces definitions. In AJV: import Ajv2020 from 'ajv/dist/2020'.

What is the difference between AJV and Zod for JSON validation?

AJV validates against JSON Schema documents — language-agnostic, shareable between Node.js, Python, Go. Zod is TypeScript-first: schemas are TypeScript code, not JSON files, automatically inferring TypeScript types via z.infer<T>. Use AJV for cross-language schema sharing or OpenAPI 3.x; use Zod for TypeScript apps where type safety and DX matter more than raw throughput.

How do I validate JSON in Python?

Use jsonschema (pip install jsonschema) with Draft202012Validator for JSON Schema compliance. For FastAPI and data-heavy apps, use Pydantic v2 — its Rust-based validation core is significantly faster and it exports JSON Schema via model.model_json_schema(). Pydantic is the standard for Python type-safe API development.

What does additionalProperties: false mean and when should I use it?

It rejects objects with keys not listed in properties. Use for API request validation — unknown fields indicate client bugs or mass-assignment attempts. Never use for response/webhook validation — forward-compatibility requires accepting new fields. For schemas composed with allOf/oneOf/anyOf, use unevaluatedProperties: false instead to correctly evaluate properties from all sub-schemas.

Can I validate JSON Schema itself (meta-validation)?

Yes. The draft 2020-12 meta-schema is at https://json-schema.org/draft/2020-12/schema. Point AJV at it to validate schema documents. VS Code does this automatically for recognized config files — it validates your tsconfig.json against its registered JSON Schema from SchemaStore (700+ schemas as of 2026), giving you real-time IntelliSense and error highlighting.

How do I get useful error messages from AJV validation failures?

AJV's validate.errors array contains instancePath (JSON Pointer to the failing value), keyword (which keyword failed), message, and params. Install ajv-errors for custom per-keyword messages, or better-ajv-errors for contextual suggestions. For APIs, map to a 422 Unprocessable Content response with a structured errors array — never expose raw AJV error objects to API clients.

Validate & Format Your JSON

Use BytePane's JSON tools to format, validate, and inspect JSON payloads before writing your schemas. Catch structural errors visually before coding validation logic.

Related Articles