ajv
Ajv is the standard JSON Schema validator for JavaScript. You give it a JSON Schema document, it compiles the schema into a plain JavaScript validation function, and that generated code makes it the fastest spec-compliant validator in the common benchmarks. It supports JSON Schema drafts 06 through 2020-12 (draft-04 via a companion package) plus JSON Type Definition, and it is the validation engine inside ESLint, webpack, Fastify, and thousands of other tools. Beyond pass/fail it can coerce types, fill in defaults, strip unknown properties, and report structured errors with schema paths.
If the schema is JSON Schema, use Ajv; it is the fastest and most compliant option and the whole tooling ecosystem already depends on it. If you are inventing schemas fresh in a TypeScript app, a type-inferring library like zod is usually the better developer experience.
Use it if
- You validate against actual JSON Schema documents: OpenAPI request bodies, JSON config files with published schemas, or contracts shared with non-JavaScript services
- Validation sits on a hot path (every API request) and you can compile schemas once at startup, because the compiled functions are dramatically faster than tree-walking validators
- You need spec behaviors that hand-rolled validators skip: $ref across schemas, recursive references, unicode-correct string lengths, or the discriminator and nullable OpenAPI keywords
- You want validation plus data massaging in one pass: coerceTypes for query strings, useDefaults for optional fields, removeAdditional for strict APIs
- You are in a TypeScript-first codebase defining schemas by hand: zod or valibot derive the static types from the schema definition, while with Ajv you maintain a JSON Schema and a TS type and hope they agree (JSONSchemaType helps but fights you on anything complex)
- Your runtime bans code generation: Ajv compiles schemas with new Function, which strict CSP and some edge runtimes reject; the standalone precompilation workaround works but adds a build step you have to maintain
- You need friendly end-user error messages: raw Ajv errors are machine-shaped (instancePath, keyword, params) and turning them into human sentences means the ajv-errors plugin plus real work
- You are still on v6-era schemas: v8 strict mode rejects many previously tolerated schemas, formats moved to a separate ajv-formats package, and the migration is a genuine chore for large schema collections
Setup reality
npm install ajv is clean, four small dependencies, no native code. The surprises come at first run: format keywords like email and date-time silently need the separate ajv-formats package or compilation throws in strict mode; strict mode itself (on by default in v8) errors on unknown keywords and suspect schema patterns that older tutorials use freely; and JSON Type Definition support lives at a different import path (ajv/dist/jtd) with a different class. TypeScript users get good types, but JSONSchemaType requires optional properties be marked nullable in ways that surprise people. CSP-restricted environments must precompile with ajv-cli.
Patterns
Compile a schema and validate datacompile-and-validate
import Ajv from "ajv";
const ajv = new Ajv();
const schema = {
type: "object",
properties: {
name: { type: "string" },
age: { type: "integer", minimum: 0 },
},
required: ["name"],
additionalProperties: false,
};
const validate = ajv.compile(schema);
if (!validate(data)) console.log(validate.errors);Compile once and reuse the function; compiling per request throws away Ajv's whole performance advantage. Errors live on validate.errors, not a return value.
Enable email, date-time, and other formatsadd-formats
import Ajv from "ajv";
import addFormats from "ajv-formats";
const ajv = new Ajv();
addFormats(ajv);
const validate = ajv.compile({ type: "string", format: "email" });Since v8, formats are not bundled. Without ajv-formats, compiling a schema that uses format throws in strict mode rather than silently passing.
Report every error instead of the firstcollect-all-errors
import Ajv from "ajv";
const ajv = new Ajv({ allErrors: true });
const validate = ajv.compile(schema);
validate(data);
// validate.errors is now an array of every failureThe docs warn allErrors can be exploited for denial of service on untrusted input, since hostile payloads can generate huge error arrays. Use it for forms, not raw internet input.
Type the schema against a TypeScript interfacetypescript-typed-schema
import Ajv, { JSONSchemaType } from "ajv";
interface User {
name: string;
age?: number;
}
const schema: JSONSchemaType<User> = {
type: "object",
properties: {
name: { type: "string" },
age: { type: "integer", nullable: true },
},
required: ["name"],
};
const validate = new Ajv().compile(schema);
if (validate(data)) {
// data is narrowed to User here
}Optional properties must be declared nullable: true in the schema or JSONSchemaType rejects it. The compiled function is a type guard, which is the main payoff.
Coerce string inputs to schema typescoerce-query-params
import Ajv from "ajv";
const ajv = new Ajv({ coerceTypes: true });
const validate = ajv.compile({
type: "object",
properties: {
page: { type: "integer" },
active: { type: "boolean" },
},
});
const q = { page: "2", active: "true" };
validate(q); // q is now { page: 2, active: true }Coercion mutates the input object in place. Handy for query strings and env vars, surprising if you did not expect your data to change.
Fill defaults and remove unknown propertiesdefaults-and-strip-unknown
import Ajv from "ajv";
const ajv = new Ajv({ useDefaults: true, removeAdditional: true });
const validate = ajv.compile({
type: "object",
properties: {
limit: { type: "integer", default: 20 },
},
additionalProperties: false,
});
const body = { limit: undefined, junk: 1 };
validate(body); // junk removed, limit set to 20removeAdditional only strips where additionalProperties: false is set, and both options mutate input. Combining removeAdditional with anyOf has documented footguns.
Share schemas with $refreference-shared-schemas
import Ajv from "ajv";
const ajv = new Ajv();
ajv.addSchema({
$id: "https://example.com/address.json",
type: "object",
properties: { city: { type: "string" } },
});
const validate = ajv.compile({
type: "object",
properties: {
home: { $ref: "https://example.com/address.json" },
},
});Remote references are never fetched over the network; every referenced schema must be added with addSchema before compile, or compilation throws.
Add a custom validation keywordcustom-keyword
import Ajv from "ajv";
const ajv = new Ajv();
ajv.addKeyword({
keyword: "evenNumber",
type: "number",
validate: (schemaVal, data) => !schemaVal || data % 2 === 0,
});
const validate = ajv.compile({ type: "number", evenNumber: true });Without registering the keyword, strict mode rejects the schema outright. Code-generating keywords are faster than validate functions but far more work.
Add a custom string formatcustom-format
import Ajv from "ajv";
const ajv = new Ajv();
ajv.addFormat("hex-color", /^#[0-9a-f]{6}$/i);
const validate = ajv.compile({ type: "string", format: "hex-color" });A regex is enough for simple cases; pass a function for logic. Beware writing formats with catastrophic backtracking if input is untrusted.
Validate with JSON Type Definition insteadjson-type-definition
import Ajv from "ajv/dist/jtd";
const ajv = new Ajv();
const schema = {
properties: {
name: { type: "string" },
},
optionalProperties: {
age: { type: "uint32" },
},
};
const validate = ajv.compile(schema);JTD is a separate, simpler schema language (RFC 8927) with its own Ajv class at a different import path. Do not mix JTD and JSON Schema keywords.
Precompile validators for CSP-restricted runtimesstandalone-precompiled
npx ajv compile -s schema.json -o validate.js --strict=true
// then at runtime, no codegen needed:
// import validate from "./validate.js";
// validate(data);This is the supported answer for environments that forbid new Function. The generated module still needs Ajv's small runtime helpers as a dependency.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| zod | npm | TypeScript-first validation where you want static types inferred from the schema instead of writing JSON Schema. |
| @exodus/schemasafe | npm | You need JSON Schema validation without eval or new Function for strict CSP environments. |
| joi | npm | Server-side validation with a chainable builder API and rich built-in messages, no JSON Schema requirement. |