Validate JSON Schema in JavaScript with Ajv
The standard library for JSON Schema validation in JavaScript is Ajv (Another JSON Schema Validator) — downloaded over 50 million times per week on npm and supporting JSON Schema drafts 04, 06, 07, 2019-09, and 2020-12. Ajv compiles schemas into optimized JavaScript validation functions at ~10× the speed of comparable libraries, making it the right choice for both high-throughput Node.js APIs and browser-side form validation. This guide covers the full Ajv workflow: installation, schema compilation, validation, error handling, adding format validators ("email", "date", "uuid"), TypeScript type inference, and async remote schema loading. See our JSON Schema tutorial and JSON Schema examples for background on the schema language itself.
Test your JSON Schema against real data in Jsonic.
Open JSON Schema ValidatorInstall Ajv and validate your first schema
Ajv 6.x (the long-term support release) targets JSON Schema draft-07 and is the most widely deployed version. Ajv 8.x adds support for drafts 2019-09 and 2020-12 with a slightly different import style. Install based on which draft your schemas use:
| Ajv version | Supported drafts | Install command |
|---|---|---|
| ajv@6 (LTS) | draft-04, 06, 07 | npm install ajv |
| ajv@8 | draft-07, 2019-09, 2020-12 | npm install ajv |
| ajv@8 + Ajv2020 | draft-2020-12 only | import Ajv2020 from "ajv/dist/2020" |
The core pattern is always the same: create one Ajv instance, compile your schema once with ajv.compile(schema), then call the returned validate() function for every data object. Compilation is the expensive step — reusing the compiled function is what makes Ajv fast.
// CommonJS
const Ajv = require("ajv");
const ajv = new Ajv(); // draft-07 by default
// ES Modules / TypeScript
// import Ajv from "ajv";
// Define a JSON Schema
const schema = {
type: "object",
properties: {
id: { type: "integer" },
name: { type: "string", minLength: 1 },
email: { type: "string" },
age: { type: "integer", minimum: 0, maximum: 150 },
},
required: ["id", "name", "email"],
additionalProperties: false,
};
// Compile ONCE — returns a reusable validate function
const validate = ajv.compile(schema);
// ── Valid data ────────────────────────────────────────────────────────────
const validData = { id: 1, name: "Alice", email: "alice@example.com", age: 30 };
const isValid = validate(validData);
console.log(isValid); // true
console.log(validate.errors); // null — no errors on success
// ── Invalid data ──────────────────────────────────────────────────────────
const invalidData = { id: "not-a-number", name: "", extra: "not allowed" };
const isValid2 = validate(invalidData);
console.log(isValid2); // false
console.log(validate.errors); // array of ErrorObject
// ── Reuse: call validate() many times, compile() only once ────────────────
const users = [
{ id: 2, name: "Bob", email: "bob@example.com" },
{ id: 3, name: "Carol", email: "carol@example.com" },
];
for (const user of users) {
if (!validate(user)) {
console.error("Invalid user:", ajv.errorsText(validate.errors));
}
}Note the additionalProperties: false keyword — without it, extra fields in the data are silently ignored. Add it when you want strict schema enforcement (e.g., validating API request bodies). For schemas with $ref references, see our guide on JSON Schema $ref and $defs.
Reading and formatting validation errors
When validation fails, validate.errors is an array of ErrorObject. Each object describes exactly what failed and where. The key fields are:
instancePath— JSON Pointer to the failing value in your data (e.g.,"/address/city")schemaPath— JSON Pointer to the failing keyword in the schema (e.g.,"#/properties/age/minimum")keyword— the JSON Schema keyword that failed (e.g.,"type","required","minimum")message— human-readable description (e.g.,"must be integer")params— keyword-specific details (e.g.,{ missingProperty: 'email' }forrequired)
const Ajv = require("ajv");
const ajv = new Ajv({ allErrors: true }); // collect ALL errors, not just the first
const schema = {
type: "object",
properties: {
name: { type: "string" },
age: { type: "integer", minimum: 0 },
score: { type: "number", maximum: 100 },
},
required: ["name", "age"],
};
const validate = ajv.compile(schema);
const badData = {
name: 42, // wrong type: should be string
// age missing // required field absent
score: 150, // exceeds maximum: 100
};
validate(badData);
// validate.errors is an array:
// [
// {
// instancePath: '/name',
// schemaPath: '#/properties/name/type',
// keyword: 'type',
// params: { type: 'string' },
// message: 'must be string'
// },
// {
// instancePath: '',
// schemaPath: '#/required',
// keyword: 'required',
// params: { missingProperty: 'age' },
// message: "must have required property 'age'"
// },
// {
// instancePath: '/score',
// schemaPath: '#/properties/score/maximum',
// keyword: 'maximum',
// params: { comparison: '<=', limit: 100 },
// message: 'must be <= 100'
// }
// ]
// ── Quick formatted string ────────────────────────────────────────────────
console.log(ajv.errorsText(validate.errors));
// "data/name must be string, data must have required property 'age', data/score must be <= 100"
// ── Map errors to user-friendly messages ─────────────────────────────────
function formatErrors(errors) {
return errors.map((err) => {
const field = err.instancePath ? err.instancePath.replace(/^//, '') : 'root';
return `${field}: ${err.message}`;
});
}
console.log(formatErrors(validate.errors));
// [ 'name: must be string', 'root: must have required property 'age'', 'score: must be <= 100' ]Pass { allErrors: true } to the Ajv constructor to collect every error in a single pass. By default Ajv stops at the first failure, which is faster but less useful for form validation where you want to show all errors at once.
Adding string format validators with ajv-formats
Ajv deliberately ignores the "format" keyword by default — validating formats with regexes can be expensive at 50M calls/week across the ecosystem, and many schemas use "format" only as documentation. To enable actual format enforcement, install the companion package:
npm install ajv-formatsThen call addFormats(ajv) once after creating your Ajv instance. This enables all 30+ built-in formats:
const Ajv = require("ajv");
const addFormats = require("ajv-formats");
const ajv = new Ajv();
addFormats(ajv); // enable "email", "date", "uri", "uuid", etc.
// ── Schema using format validation ────────────────────────────────────────
const userSchema = {
type: "object",
properties: {
email: { type: "string", format: "email" },
birthDate: { type: "string", format: "date" }, // YYYY-MM-DD
website: { type: "string", format: "uri" },
userId: { type: "string", format: "uuid" },
updatedAt: { type: "string", format: "date-time" }, // ISO 8601
},
required: ["email"],
};
const validate = ajv.compile(userSchema);
// Valid
console.log(validate({ email: "alice@example.com", birthDate: "1990-06-15" })); // true
// Invalid email
validate({ email: "not-an-email" });
console.log(validate.errors[0].message); // 'must match format "email"'
// Invalid date (month 13 does not exist)
validate({ email: "a@b.com", birthDate: "1990-13-01" });
console.log(validate.errors[0].message); // 'must match format "date"'
// ── Available formats ─────────────────────────────────────────────────────
// "date" YYYY-MM-DD
// "date-time" ISO 8601 with time zone
// "time" HH:mm:ss[.sss]Z
// "email" RFC 5321 email address
// "idn-email" Internationalized email
// "hostname" RFC 1034 hostname
// "idn-hostname" Internationalized hostname
// "ipv4" Dotted-decimal IPv4
// "ipv6" IPv6 address
// "uri" RFC 3986 URI
// "uri-reference" URI reference
// "url" URL (more permissive than uri)
// "uuid" UUID v1–v5 (RFC 4122)
// "json-pointer" JSON Pointer (RFC 6901)
// ── Custom format ─────────────────────────────────────────────────────────
ajv.addFormat("positiveNumericString", {
type: "string",
validate: (value) => /^[1-9][0-9]*$/.test(value),
});
const schema2 = { type: "string", format: "positiveNumericString" };
const v2 = ajv.compile(schema2);
console.log(v2("42")); // true
console.log(v2("0")); // false
console.log(v2("abc")); // falseCustom keywords and async validation
When JSON Schema's built-in keywords aren't enough for your business logic — for example, checking that a value is divisible by a specific number, or verifying uniqueness against a database — Ajv lets you add custom keywords and async validators.
Custom keyword: divisibleBy
const Ajv = require("ajv");
const ajv = new Ajv();
// Add a custom "divisibleBy" keyword
ajv.addKeyword({
keyword: "divisibleBy",
type: "number",
schemaType: "number",
validate: function divisibleBy(schema, data) {
return data % schema === 0;
},
errors: false,
});
const schema = {
type: "object",
properties: {
quantity: { type: "integer", divisibleBy: 5 }, // must be multiple of 5
price: { type: "number", divisibleBy: 0.01 }, // max 2 decimal places
},
};
const validate = ajv.compile(schema);
console.log(validate({ quantity: 10, price: 9.99 })); // true
console.log(validate({ quantity: 7, price: 9.99 })); // false — 7 % 5 !== 0Async validation: database uniqueness check
const Ajv = require("ajv");
const ajv = new Ajv();
// Simulate an async DB check
async function isEmailUnique(email) {
// In production: await db.users.findOne({ email })
const takenEmails = ["admin@example.com", "support@example.com"];
return !takenEmails.includes(email);
}
// Add async custom keyword
ajv.addKeyword({
keyword: "uniqueEmail",
async: true, // mark this keyword as async
type: "string",
validate: isEmailUnique,
});
// Mark the schema as async with $async: true
const schema = {
$async: true,
type: "object",
properties: {
email: { type: "string", format: "email", uniqueEmail: true },
name: { type: "string" },
},
required: ["email", "name"],
};
// compileAsync returns a Promise<ValidateFunction>
const validateAsync = ajv.compile(schema);
// Usage — the compiled function returns a Promise<boolean>
async function registerUser(data) {
try {
const valid = await validateAsync(data);
if (valid) {
console.log("User data is valid");
}
} catch (err) {
if (err instanceof Ajv.ValidationError) {
// Async validation failed
console.error(ajv.errorsText(err.errors));
} else {
throw err; // unexpected error (DB connection failure, etc.)
}
}
}
registerUser({ email: "alice@example.com", name: "Alice" }); // valid
registerUser({ email: "admin@example.com", name: "Admin" }); // ValidationErrorUse async validation sparingly — it's best for cross-field uniqueness checks that require a DB round-trip. For everything achievable with pure data inspection (type, length, pattern, range), stick with synchronous validation for maximum performance. See also: oneOf, anyOf, allOf for combining schemas without custom keywords.
Using Ajv with TypeScript
Ajv ships with TypeScript types. The most powerful feature is JSONSchemaType<T> — a utility type that enforces your JSON Schema matches your TypeScript interface at compile time, catching mismatches before you run a single validation.
import Ajv, { JSONSchemaType } from "ajv";
const ajv = new Ajv();
// Define the TypeScript interface
interface User {
id: number;
name: string;
email: string;
age?: number; // optional field
}
// JSONSchemaType<User> makes TypeScript verify the schema matches User
// TypeScript will error if, e.g., you write { type: "string" } for the id field
const schema: JSONSchemaType<User> = {
type: "object",
properties: {
id: { type: "number" },
name: { type: "string" },
email: { type: "string" },
age: { type: "number", nullable: true }, // optional fields need nullable: true
},
required: ["id", "name", "email"],
additionalProperties: false,
};
// compile() returns ValidateFunction<User> — a typed validate function
const validate = ajv.compile(schema);
function processUser(data: unknown): User {
// validate() acts as a TypeScript type guard
if (validate(data)) {
// Inside this block, TypeScript knows data is User
console.log(data.name.toUpperCase()); // safe — TypeScript knows name is string
return data;
} else {
throw new Error(ajv.errorsText(validate.errors));
}
}
// ── Parsing unknown API responses ─────────────────────────────────────────
async function fetchUser(id: number): Promise<User> {
const res = await fetch(`/api/users/${id}`);
const raw: unknown = await res.json();
if (!validate(raw)) {
throw new Error(`Invalid API response: ${ajv.errorsText(validate.errors)}`);
}
return raw; // TypeScript: raw is User
}JSONSchemaType covers draft-07 keywords only. If you use draft-2019-09 or 2020-12 features like unevaluatedProperties, use the looser JTDSchemaType (JSON Type Definition) or a library like Zod that generates both the schema and TypeScript types from a single source of truth. For schemas with cross-references, see the JSON Schema $ref and $defs guide.
Frequently asked questions
How do I install and use Ajv in JavaScript?
Install with npm install ajv for JSON Schema draft-07 support. Create an instance, compile your schema, then call the compiled function:
const Ajv = require('ajv');
const ajv = new Ajv();
const validate = ajv.compile(schema);
const valid = validate(data);
if (!valid) console.log(validate.errors);The validate function is reusable — compile once and call it for every data object you need to validate. For ES modules, import as: import Ajv from "ajv".
What JSON Schema drafts does Ajv support?
Ajv 6.x supports JSON Schema drafts 04, 06, and 07 (the most common). Ajv 8.x supports draft-07, draft-2019-09, and draft-2020-12 — use new Ajv() for draft-07 (default), or import the specific class: import Ajv2019 from "ajv/dist/2019" for draft-2019-09, import Ajv2020 from "ajv/dist/2020" for draft-2020-12. Most production schemas use draft-07, which is supported by both major versions. See the JSON Schema tutorial for an overview of which draft to choose.
How do I read Ajv validation errors?
When validation fails, validate.errors is an array of ErrorObject. Each object has: instancePath (the path in your data, e.g., "/address/city"), schemaPath (the path in the schema), keyword (the failed keyword, e.g., "type", "required", "minimum"), message (human-readable, e.g., "must be string"), and params (keyword-specific details). Use ajv.errorsText(validate.errors) to get a single formatted string of all errors.
How do I validate email, URL, or date format with Ajv?
Ajv ignores the "format" keyword by default for performance. To enable format validation, install ajv-formats (npm install ajv-formats) and call addFormats(ajv). Available formats include "email", "uri", "url", "date" (YYYY-MM-DD), "date-time" (ISO 8601), "uuid" (UUID v1-v5), "hostname", "ipv4", "ipv6", and more. You can also add custom formats with ajv.addFormat('myformat', (value) => boolean).
How do I use Ajv with TypeScript?
Import the JSONSchemaType utility: import Ajv, { JSONSchemaType } from "ajv". Define your schema with the type parameter: const schema: JSONSchemaType<User> = { type: "object", properties: { id: { type: "number" }, name: { type: "string" } }, required: ["id", "name"] }. TypeScript will error if the schema doesn't match the User interface. The compiled validate function then acts as a type guard: inside if (validate(data)) { /* TypeScript knows data is User here */ }.
What is the difference between Ajv and the jsonschema npm package?
Both validate JSON against JSON Schema, but Ajv is the industry standard: it's 10–50× faster (schema compilation vs interpretation), downloaded 50M times/week vs ~500K for jsonschema, and actively maintained with support for JSON Schema drafts up to 2020-12. The jsonschema package uses a simpler API (validate(data, schema)) and is easier to use for one-off checks, but it doesn't generate compiled validators and has limited TypeScript support. For any production use, Ajv is the standard choice. You can try both approaches interactively in the JSON Schema validator tool.
Validate your JSON Schema online
Paste any JSON and schema into Jsonic's validator to check compliance instantly — no install required.
Open JSON Schema Validator