Skip to content
Back to Blog
Tutorials

JSON Schema validation in 2026: Ajv, Python, browser guide

Validate JSON against a schema in Node, Python, and the browser. Draft 2020-12 essentials, real API patterns, and copy-paste examples for 2026. Try free.

12 min read

JSON Schema validation: validate JSON in Node, Python, and the browser (2026)

TL;DR: a JSON Schema is a contract for JSON data. You declare field types, required keys, and constraints, then a validator checks whether a document follows that contract. Use Ajv in Node for the fastest validation, the jsonschema library in Python for portable schemas, and bundle Ajv in the browser for instant form and config feedback. For new projects in 2026, pick Draft 2020-12.

This guide walks through the smallest working example, end-to-end patterns in all three runtimes, and the traps behind “validation passes but production rejects the data” bugs.

What JSON Schema is (and is not)

A one-sentence definition

A JSON Schema is a JSON document that describes the shape of other JSON documents. A validator reads the schema and the data, then confirms conformance or returns the failing paths.

The smallest useful example:

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

{"name": "Alice"} passes. {"age": 30} fails (missing name). {"name": 42} fails (name is not a string). That is the entire mental model.

JSON Schema vs JSON syntax validation

These are two different problems and they get confused all the time.

DimensionJSON syntax checkJSON Schema validation
What it checksIs this a legal JSON document?Does this JSON match the contract?
CatchesMissing commas, single quotes, commentsWrong types, missing required fields, out-of-range values
ToolsJSON.parse(), JSON FormatterAjv, jsonschema (Python), fastjsonschema
When you reach for itFirst thing, before parsingRight after parsing, before business logic

In practice you do both. Pretty-print the payload in JSON Formatter to confirm it parses, then run it through a schema to confirm it matches the contract.

JSON Schema vs JSONPath, JSON Patch, jq, and TypeScript

Five tools share this problem space. Here is how to choose.

ToolQuestion it answersReach for it when
JSON SchemaDoes this JSON match the expected structure?Validating API input, config files, form payloads
JSONPathHow do I query a value out of this JSON?Extracting nested fields, batch reads
JSON Patch (RFC 6902)How do I describe the diff from A to B?Collaborative editing, incremental sync
jqHow do I process JSON on the command line?Shell scripts, log pipelines, CI checks
TypeScript typesDoes my code use this shape correctly?Compile-time guarantees inside one codebase

The split that matters: JSON Schema validates unknown data at runtime, while TypeScript validates known code at compile time. TypeScript can’t help you with JSON arriving from a third-party webhook or a user paste, and that is exactly the gap JSON Schema fills. Zod and Pydantic sit in the middle (compile-time types plus runtime validation), and we cover them below.

JSON Schema vs OpenAPI

A common misconception is that OpenAPI replaces JSON Schema. It doesn’t. OpenAPI uses JSON Schema internally to describe request and response bodies, then layers paths, parameters, security schemes, and server URLs on top. The schema is the data-shape contract; OpenAPI is the API contract that wraps it.

DimensionJSON SchemaOpenAPI
ScopeShape of one JSON documentShape of an entire HTTP API
DependenciesNone (schema is self-contained JSON)Imports JSON Schema for body definitions
Version pairingDraft 7 / Draft 2019-09 / Draft 2020-12OpenAPI 3.0 uses a Draft 4 subset; OpenAPI 3.1 uses Draft 2020-12 natively
Typical useConfig files, message envelopes, form validation, single-payload contractsREST API design, SDK generation, mock servers, contract testing
Code generationLimited (some quicktype-style tools)Mature ecosystem (openapi-generator, oapi-codegen, vendor SDKs)
Contract managementOne file per shape, no routingPaths, operations, auth flows, versioned endpoints in one document

Reach for plain JSON Schema when the artifact you care about is a single document: a webhook payload, a config file, a queue message, or a form. There’s no HTTP surface to describe, so OpenAPI is overhead.

Reach for OpenAPI when you’re publishing an HTTP API and want one document to drive docs, SDK generation, mock servers, and contract tests. Define your schemas first as standalone JSON Schema files in a schemas/ directory, then $ref them from the OpenAPI document. That keeps the schemas reusable outside the API context.

The version pairing trips teams up. OpenAPI 3.0 uses a Draft 4 subset, so you cannot use Draft 2020-12 keywords like prefixItems or unevaluatedProperties inside a 3.0 document — the generators will silently ignore them. OpenAPI 3.1 is a superset of Draft 2020-12, so anything valid in 2020-12 is valid in 3.1. If you have the choice, target OpenAPI 3.1 and write Draft 2020-12 schemas everywhere.

Your first JSON Schema (5 minutes)

The keywords you need first

You’ll get 80% of the way with these:

{
  "type": "object",
  "properties": {
    "id":       { "type": "integer", "minimum": 1 },
    "email":    { "type": "string", "format": "email" },
    "age":      { "type": "integer", "minimum": 0, "maximum": 150 },
    "tags":     { "type": "array", "items": { "type": "string" }, "minItems": 1 },
    "role":     { "enum": ["admin", "editor", "viewer"] },
    "metadata": { "type": "object", "additionalProperties": true }
  },
  "required": ["id", "email"],
  "additionalProperties": false
}

The vocabulary:

  • type: one of string, number, integer, boolean, null, array, object
  • properties plus required: declare fields and mark which must be present
  • enum / const: restrict to a fixed set or single literal
  • minimum / maximum / multipleOf: numeric bounds
  • minLength / maxLength / pattern: string length and regex
  • minItems / maxItems / uniqueItems: array shape
  • additionalProperties: false: reject undeclared keys (always set this on input contracts)

JSON Schema examples by use case

The keywords above show up in different combinations depending on what you’re validating. A few representative shapes:

API request body — a signup endpoint that accepts email and password:

{
  "type": "object",
  "properties": {
    "email":    { "type": "string", "format": "email" },
    "password": { "type": "string", "minLength": 8, "maxLength": 128 }
  },
  "required": ["email", "password"],
  "additionalProperties": false
}

Configuration file — a logger config that locks the level to a fixed set:

{
  "type": "object",
  "properties": {
    "level":  { "enum": ["debug", "info", "warn", "error"] },
    "output": { "type": "string", "default": "stdout" }
  },
  "required": ["level"],
  "additionalProperties": false
}

Form payload with conditional rules — when accountType is "business", taxId becomes required:

{
  "type": "object",
  "properties": {
    "accountType": { "enum": ["personal", "business"] },
    "taxId":       { "type": "string" }
  },
  "if":   { "properties": { "accountType": { "const": "business" } } },
  "then": { "required": ["taxId"] }
}

CSV-row JSON record — one row of an exported orders table:

{
  "type": "object",
  "properties": {
    "orderId":   { "type": "string", "pattern": "^ORD-[0-9]{6}$" },
    "orderedOn": { "type": "string", "format": "date" },
    "totalUsd":  { "type": "number", "minimum": 0 }
  },
  "required": ["orderId", "orderedOn", "totalUsd"]
}

Webhook event envelope — oneOf discriminates by the type literal, so each event variant has its own payload shape:

{
  "oneOf": [
    { "properties": { "type": { "const": "order.created" }, "data": { "$ref": "#/$defs/order" } } },
    { "properties": { "type": { "const": "order.refunded" }, "data": { "$ref": "#/$defs/refund" } } }
  ]
}

These five examples cover the bulk of what teams write in practice. Copy the closest match and adjust field names — the keyword vocabulary stays the same.

Validate without installing anything

Paste schema and payload into the playground at ajv.js.org or jsonschemavalidator.net for an instant verdict. If the JSON itself looks suspect, run it through JSON Formatter first.

Validating in Node.js with Ajv

Install and a 12-line example

Ajv compiles your schema into an optimized function on first compile, then reuses it.

npm install ajv
import Ajv from "ajv";
const ajv = new Ajv();

const schema = {
  type: "object",
  properties: {
    name: { type: "string" },
    age:  { type: "integer", minimum: 0 }
  },
  required: ["name"]
};

const validate = ajv.compile(schema);
const data = { name: "Alice", age: 30 };

if (!validate(data)) console.log(validate.errors);
else                  console.log("OK");

Switching to Draft 2020-12

The default Ajv constructor is still pinned to Draft 7 for backward compatibility. Opt into 2020-12 explicitly:

import Ajv2020 from "ajv/dist/2020";
const ajv = new Ajv2020({ strict: true, allErrors: true });

Now prefixItems, unevaluatedProperties, and $dynamicRef are available. See the Draft 2020-12 section below for what each one does.

Turning on format validation

This trips more developers than any other Ajv quirk: format: "email" does nothing by default. The spec treats format as advisory, so you have to register the format module:

npm install ajv-formats
import addFormats from "ajv-formats";
addFormats(ajv);  // now "format": "email" actually validates

Without this step, {"email": "not-an-email"} passes a schema that requires format: "email". Always install ajv-formats in production.

An Express middleware that actually runs in production

One validator per route, compiled at boot:

import express from "express";
import Ajv2020 from "ajv/dist/2020";
import addFormats from "ajv-formats";

const ajv = new Ajv2020({ allErrors: true });
addFormats(ajv);

const validateUser = ajv.compile({
  type: "object",
  properties: {
    email: { type: "string", format: "email" },
    age:   { type: "integer", minimum: 13 }
  },
  required: ["email"],
  additionalProperties: false
});

const app = express();
app.use(express.json());

app.post("/users", (req, res) => {
  if (!validateUser(req.body)) {
    return res.status(400).json({ errors: validateUser.errors });
  }
  // ... business logic
  res.status(201).json({ ok: true });
});

The most expensive mistake to make here is calling ajv.compile(schema) inside the request handler. Compile once at module scope and reuse the returned function. Recompiling per request drops throughput 50x or more.

Validating in Python with jsonschema

Install and basic usage

pip install jsonschema
from jsonschema import validate, ValidationError

schema = {
    "type": "object",
    "properties": {
        "name": {"type": "string"},
        "age":  {"type": "integer", "minimum": 0}
    },
    "required": ["name"]
}

try:
    validate(instance={"name": "Alice", "age": 30}, schema=schema)
    print("OK")
except ValidationError as e:
    print("FAIL:", e.message, "at", list(e.absolute_path))

Collecting all errors with Draft202012Validator

validate() throws on the first error. To list every issue at once (useful for form responses), use iter_errors:

from jsonschema import Draft202012Validator

validator = Draft202012Validator(schema)
errors = sorted(validator.iter_errors(instance), key=lambda e: e.path)
for err in errors:
    print(f"  - {'/'.join(map(str, err.absolute_path))}: {err.message}")

Now the user fixes everything at once instead of cycling through round trips.

jsonschema vs Pydantic: when to pick which

Two strong Python libraries, two different problems.

DimensionjsonschemaPydantic v2
Schema formatA JSON dict (schema is data)A Python class with type hints
PerformanceInterpreted, ~10–100× slower than PydanticRust core, the fastest in the ecosystem
Cross-language portabilityYes (same schema works in JS, Go, Rust)No (Python only)
FastAPI / native model integrationManual conversionBuilt in
Full Draft 2020-12 keywords ($dynamicRef, etc.)CompletePartial

The rule that holds up in production is to use jsonschema for cross-language contracts (OpenAPI, public APIs, webhooks) and Pydantic for internal Python services. Plenty of teams run both: jsonschema at the gateway for contract enforcement, Pydantic at the application layer for typed business logic. The schema is the portable artifact, identical to what you would feed Ajv.

Validating in the browser

Why validate client-side at all

There are three reasons, in order of importance. Instant feedback as the user types beats a server round trip, so the UX is better. Obvious mistakes never leave the browser, which saves bandwidth. And client-side validation cuts garbage volume to the backend, which is good security hygiene but does not replace server validation. Never trust client-side validation alone; validate again on the server.

Bundling Ajv for the browser

npm install ajv ajv-formats
import Ajv2020 from "ajv/dist/2020";
import addFormats from "ajv-formats";

const ajv = new Ajv2020({ allErrors: true });
addFormats(ajv);

export const validateForm = ajv.compile({
  type: "object",
  properties: {
    email:    { type: "string", format: "email" },
    password: { type: "string", minLength: 8 }
  },
  required: ["email", "password"]
});

The bundle adds about 30 KB gzipped, which is meaningful but not catastrophic. Teams ship Ajv when they want one schema definition shared between server and client.

Lighter alternatives: Zod and Valibot

If you don’t need the JSON Schema ecosystem and you’re already in TypeScript, a TS-native validator gives you smaller bundles and tighter type inference:

import { z } from "zod";
const UserSchema = z.object({
  email: z.string().email(),
  password: z.string().min(8)
});
const result = UserSchema.safeParse(data);
if (!result.success) console.log(result.error.issues);

Valibot ships in roughly 3 KB gzipped with a similar API. Pick it when bundle size dominates. The catch is that neither library produces JSON Schema. If you need one source of truth shared with backends, third-party clients, or OpenAPI generators, stay on Ajv. If everything is your own TypeScript, Zod and Valibot are more ergonomic.

What Draft 2020-12 adds

prefixItems for tuple validation

Draft 7 expressed tuples through items: [] plus additionalItems. Draft 2020-12 splits them cleanly:

{
  "type": "array",
  "prefixItems": [
    { "type": "string" },
    { "type": "number" }
  ],
  "items": false
}

["x", 42] passes. ["x", 42, "extra"] fails. The schema reads as exactly what it does.

unevaluatedProperties for composed schemas

There is a subtle bug that bites every team using allOf or oneOf: additionalProperties: false only checks the immediate level it appears on. Sibling subschemas inside allOf declare whatever properties they like. The 2020-12 fix is unevaluatedProperties: false:

{
  "allOf": [
    { "$ref": "#/$defs/base" }
  ],
  "unevaluatedProperties": false
}

This rejects any property not evaluated by any branch, which is the behavior most developers expect from additionalProperties: false.

$dynamicRef for recursive schemas

Anyone who has tried to declare a recursive tree schema in Draft 7 knows the contortions involved. $dynamicRef plus $dynamicAnchor cleans this up:

{
  "$dynamicAnchor": "node",
  "type": "object",
  "properties": {
    "value": { "type": "string" },
    "children": { "type": "array", "items": { "$dynamicRef": "#node" } }
  }
}

The recursion is declarative and overrideable from descendants without a $id rewrite.

Draft 7 vs 2020-12: which to pick

For a new project on a modern toolchain, pick Draft 2020-12. If you are building or consuming OpenAPI 3.1, 2020-12 is the native dialect. If you are working with OpenAPI 3.0 or older services, you need Draft 4, since OpenAPI 3.0 uses a Draft 4 subset and you should not mix dialects. If you need broad validator compatibility (Postman, older CI tools), Draft 7 is still the safest interchange format.

Every modern validator (Ajv, Python jsonschema, jsonschema-rs, Java’s networknt/json-schema-validator) supports 2020-12 today.

Real-world patterns

API input validation

The Express middleware above is roughly the shape that runs in production. Two practices on top of it: keep all schemas in a schemas/ directory at the repo root, and add a CI step that runs ajv test (or the Python equivalent) to validate the schemas themselves against the JSON Schema meta-schema.

Configuration files

Visual Studio Code ships with SchemaStore integration, giving you autocomplete and inline validation for package.json, tsconfig.json, and dozens of others. Add a $schema field to your own configs and editor users get the same treatment.

CI test fixtures

Test fixtures rot. Someone updates a model, a fixture stays at the old shape, and the test still passes because the assertions never touched the changed field. Catch this with a schema check before the assertions run:

import { glob } from "glob";
const files = await glob("__tests__/fixtures/*.json");
for (const f of files) {
  const data = JSON.parse(await fs.readFile(f, "utf8"));
  if (!validate(data)) throw new Error(`${f}: ${ajv.errorsText(validate.errors)}`);
}

When the schema check fires, the next move is usually a structural diff. Pull the fixture into JSON Diff against a fresh production sample to see what drifted. If timestamps and IDs dominate the diff, apply the snapshot path-ignore patterns from the JSON diff guide to separate signal from noise.

Webhook payloads (Stripe, GitHub)

Third-party webhooks are one of the most useful places to apply JSON Schema. The webhook is a contract, the provider can change it, and you want to know the moment they do. Stripe and GitHub publish OpenAPI descriptions you can extract JSON Schemas from. Validate inbound events, and a breaking upgrade trips your monitoring instead of corrupting state quietly.

Schema-driven form validation

React Hook Form has an @hookform/resolvers/ajv adapter, and Vue’s VeeValidate has an equivalent Ajv plugin. Both drive form rendering, error messages, and submission validation from one JSON Schema, so the UI inherits its rules from the schema.

Friendly error messages

Why the defaults are rough

Out of the box, Ajv produces errors like #/properties/email format must match "email". That is fine for an engineer debugging a 400, but useless for a user filling in a checkout form.

ajv-errors for custom messages

npm install ajv-errors
import ajvErrors from "ajv-errors";
ajvErrors(ajv);

const schema = {
  type: "object",
  properties: { email: { type: "string", format: "email" } },
  required: ["email"],
  errorMessage: {
    properties: { email: "Please enter a valid email address" },
    required: { email: "Email is required" }
  }
};

The errorMessage keyword stays inside the schema, so validation rules and user-facing copy travel together.

ajv-i18n for translated errors

ajv-i18n ships translations of the default messages in 30+ languages. One line at startup and your validator speaks Spanish, French, Japanese, or whichever locales you serve. Handy as a fallback when your errorMessage overrides don’t cover every constraint.

Mapping schema paths to form fields

Each Ajv error has an instancePath like /users/0/email. Most form libraries expect dotted paths like users[0].email. One-liner:

const fieldPath = error.instancePath.replace(/^\//, "").replace(/\//g, ".");

In Python’s jsonschema, the equivalent lives at error.absolute_path. Join with . for the same effect.

Five traps that pass validation then crash production

1. format is advisory by default

Without ajv-formats plus addFormats(ajv), every format keyword is a no-op. {"format": "email"} accepts "not-an-email". Always install the format package in production.

2. additionalProperties defaults to true

Without additionalProperties: false, your schema accepts every undeclared field. Clients can ship extra fields that bypass validation entirely. Make additionalProperties: false the default on input contracts and relax deliberately when needed.

3. additionalProperties doesn’t compose

Inside allOf, oneOf, or anyOf, additionalProperties: false only inspects properties at its own level. Sibling subschemas slip through. The Draft 2020-12 fix is unevaluatedProperties: false.

4. Remote $ref is a production risk

$ref: "https://example.com/schema.json" makes Ajv fetch over the network on first compile. That means latency, DoS exposure if the remote host stalls, and an MITM attack surface. Inline all $ref targets or load them from disk at build time.

5. Generated schemas drift from real data

Tools like quicktype and typescript-json-schema generate schemas from existing types. The output usually over-permits, with every field optional and additionalProperties open. Treat generated schemas as drafts, tighten by hand, and run CI that validates real production samples against the schema (and vice versa) so drift surfaces fast.

Performance: numbers and rules of thumb

  • Ajv (Node.js): compiled validators clear one check in well under a microsecond. The fastest production-grade JS validator available.
  • jsonschema (Python): interpreted, 10-100x slower than Pydantic. Swap in fastjsonschema when this bites; it generates Python code and lands close to Ajv.
  • Rust and Go: jsonschema-rs and xeipuuv/gojsonschema give you another 2-5x over Ajv at the gateway tier.
  • The biggest win is to precompile. Call ajv.compile(schema) once at module load and reuse the returned validator on every request. Recompiling per request kills throughput by 50x or more.

Frequently asked questions

What is JSON Schema validation in plain English?

JSON Schema validation checks whether a JSON document follows a contract. The contract (the schema) is itself JSON, declaring types, required fields, and constraints. A validator reads the schema and the data and reports “passes” or the paths that failed and why.

How do I validate JSON against a schema online?

Paste schema and data into the ajv.js.org playground or jsonschemavalidator.net for an instant verdict. If the JSON looks malformed, clean it up in JSON Formatter first; both run in the browser, no upload.

Which JSON Schema validator is fastest in 2026?

In Node, Ajv with precompiled validators clears one check in under a microsecond. In Python, fastjsonschema generates code and reaches Ajv-class throughput. At the gateway tier, jsonschema-rs (Rust) and gojsonschema (Go) are 2-5x faster than Ajv. Whichever you pick, precompile once and reuse.

What is the difference between JSON Schema and TypeScript types?

TypeScript checks the code you write at compile time. JSON Schema checks unknown JSON at runtime. TypeScript can’t see JSON arriving from an HTTP response, a file, or a user paste; that’s exactly what JSON Schema is for.

Should I use Draft 2020-12 or Draft 7?

For new projects in 2026, pick Draft 2020-12. prefixItems, unevaluatedProperties, and $dynamicRef solve real problems, and OpenAPI 3.1 uses 2020-12 natively. Stay on Draft 7 only for Postman compatibility or older services. OpenAPI 3.0 uses a Draft 4 subset, so don’t mix dialects.

How do I generate a JSON Schema from existing JSON?

You have three options. Paste samples into quicktype.io or jsonschema.net, run npx genson-js or pip install genson && genson sample.json on the command line, or hand-write it. Auto-generated schemas are too permissive (every field optional, additionalProperties: true), so always tighten before treating them as contracts.

Can JSON Schema replace OpenAPI?

No. OpenAPI uses JSON Schema internally to describe request and response bodies, then adds paths, security schemes, parameters, and server URLs. They compose: write your schemas, reference them from an OpenAPI document, get full API contracts.

Is JSON Schema the same as JSONPath or jq?

Different problems. JSON Schema validates structure (“does this JSON match the contract?”). JSONPath and jq extract values (“every pod name in the Running phase”). Validate with a schema; query with JSONPath or jq.

Why does my Ajv validation pass but production rejects the data?

Three culprits cover almost every case. You forgot ajv-formats, so format: "email" never validated. You omitted additionalProperties: false, so extra client fields slipped through. Or you used additionalProperties: false inside allOf or oneOf and discovered it doesn’t compose; the fix there is to switch to unevaluatedProperties: false.

Can I customize JSON Schema error messages for end users?

Yes. In Node, install ajv-errors to embed errorMessage inside the schema, and ajv-i18n for translations across 30+ locales. In Python, jsonschema exposes the full validation context on each error object, so you can map error type plus path to whatever copy your design system uses.

Related Articles

View all articles