Ajv v8 JSON Schema Validator: Complete Guide
Ajv (Another JSON Validator) is the fastest JSON Schema validator for JavaScript, compiling schemas to optimized JavaScript functions that run 3–10× faster than interpret-based validators. Ajv v8 (released 2021) supports JSON Schema Draft-07 and Draft 2020-12 — compile once with ajv.compile(schema) and reuse the validate function everywhere. Compiling on every call is the single most common performance mistake, adding 10–100 ms per request under load.
This guide covers installation, compiling and reusing validators, reading error objects, adding string formats with ajv-formats, async validation, TypeScript type inference with JTD, and custom keywords — everything needed to validate JSON reliably and fast in production.
Want to check a JSON Schema right now? Jsonic's JSON Schema Validator uses Ajv under the hood and shows exact error paths when validation fails.
Validate JSON Schema in JsonicInstallation and basic setup
Ajv v8 requires Node.js 12+ and is available as an npm package. Install Ajv and, separately, the ajv-formats plugin for string format validation — formats like email, uri, and date are not bundled in Ajv v8 by default to keep the core small.
npm install ajv ajv-formatsCreate an Ajv instance once per application, not per request. Pass options at construction time — they cannot be changed after the fact:
import Ajv from 'ajv'
import addFormats from 'ajv-formats'
// Create once at module level
const ajv = new Ajv({
allErrors: true, // collect ALL errors, not just the first
strict: true, // warn on unknown keywords (default in v8)
coerceTypes: false, // don't convert "42" string to number 42
})
addFormats(ajv) // enable email, uri, date, uuid, ipv4, …
export default ajvThe 3 most important constructor options are allErrors (collect every failing constraint so users see all problems at once, not just the first), strict (catch schema authoring mistakes like typos in keyword names), and coerceTypes (only enable if you're validating form data where numbers arrive as strings).
| Option | Default | Effect |
|---|---|---|
allErrors | false | Stop at first error (false) or collect all errors (true) |
strict | true | Throw on unknown keywords and ambiguous schemas |
coerceTypes | false | Auto-coerce types (string "1" → number 1); mutates input data |
useDefaults | false | Write default values into validated objects |
removeAdditional | false | Strip properties not in additionalProperties; mutates input |
Compiling and reusing validators for performance
Ajv's key insight is that ajv.compile(schema) is expensive — it walks the entire schema, generates a JavaScript function string, and evaluates it. Benchmarks show this takes 10–100 ms per schema depending on complexity. Calling compile on every incoming request caps throughput at 10–100 req/sec regardless of how fast your server hardware is.
The correct pattern: call ajv.compile() once at module load time, store the returned validate function, and reuse it for every piece of data:
// schemas/user.ts — compiled ONCE at module load
import ajv from '../lib/ajv'
const userSchema = {
type: 'object',
properties: {
id: { type: 'integer', minimum: 1 },
email: { type: 'string', format: 'email' },
name: { type: 'string', minLength: 1, maxLength: 100 },
role: { type: 'string', enum: ['admin', 'user', 'guest'] },
},
required: ['id', 'email', 'name', 'role'],
additionalProperties: false,
} as const
// validate is compiled once and reused
export const validateUser = ajv.compile(userSchema)
// In your request handler (runs thousands of times/sec):
export function handleCreateUser(body: unknown) {
if (!validateUser(body)) {
return { error: 'Invalid user', details: validateUser.errors }
}
// body is valid here — proceed
}With this pattern, Ajv achieves 90,000–170,000 validations per second for typical object schemas, compared to 10,000–30,000 for interpret-based validators and 1,000–5,000 when compile is mistakenly called per-request. That is a 30–100× difference at scale.
If you need to validate against schemas you receive at runtime (e.g., from a database), cache the compiled validators in a Map keyed by schema ID or hash — compile on first use, cache thereafter.
Reading and displaying Ajv error objects
When validate(data) returns false, the validate.errors array contains 1 error object per failed constraint (or just 1 if allErrors: false). Understanding the 5 fields on each error object is essential for building useful error messages.
// Example: validate a user with missing email and too-short name
validateUser({ id: 1, name: '', role: 'unknown' })
// validate.errors:
[
{
instancePath: '', // root object — 'email' is missing
schemaPath: '#/required',
keyword: 'required',
params: { missingProperty: 'email' },
message: "must have required property 'email'",
},
{
instancePath: '/name', // JSON Pointer to the failing field
schemaPath: '#/properties/name/minLength',
keyword: 'minLength',
params: { limit: 1 },
message: 'must NOT have fewer than 1 characters',
},
{
instancePath: '/role',
schemaPath: '#/properties/role/enum',
keyword: 'enum',
params: { allowedValues: ['admin', 'user', 'guest'] },
message: 'must be equal to one of the allowed values',
},
]The 4 fields you use most are: instancePath (JSON Pointer to the failing field in the data — empty string means the root), keyword (which constraint failed), message (human-readable text), and params (structured detail, varies by keyword). Use instancePath to map errors to form fields:
// Convert Ajv errors to a field → message map for UI display
function ajvErrorsToFieldMap(errors: typeof validate.errors) {
const map: Record<string, string> = {}
for (const err of errors ?? []) {
// instancePath '/name' → field key 'name'
// instancePath '' → field key '_root'
const field = err.instancePath.replace(/^//, '') || '_root'
// For 'required' errors, instancePath is the parent; use missingProperty
const key = err.keyword === 'required'
? (err.params as { missingProperty: string }).missingProperty
: field
map[key] = err.message ?? 'Invalid value'
}
return map
}
// Result: { email: "must have required property 'email'", name: '...', role: '...' }For a quick single string, use ajv.errorsText(validate.errors) — it joins all messages with a semicolon. This is useful for logging but too terse for end users.
Adding string formats with ajv-formats
Ajv v8 deliberately removed built-in format validation to avoid pulling in large regex libraries into every bundle. The ajv-formats plugin adds 30+ formats as an opt-in. Install and apply it once to your shared Ajv instance and all compiled validators inherit format support automatically.
import Ajv from 'ajv'
import addFormats from 'ajv-formats'
const ajv = new Ajv()
addFormats(ajv) // call BEFORE any ajv.compile()
const schema = {
type: 'object',
properties: {
email: { type: 'string', format: 'email' },
homepage: { type: 'string', format: 'uri' },
birthDate: { type: 'string', format: 'date' }, // "2025-05-12"
updatedAt: { type: 'string', format: 'date-time' }, // ISO 8601
sessionId: { type: 'string', format: 'uuid' },
serverIp: { type: 'string', format: 'ipv4' },
},
}
const validate = ajv.compile(schema)
validate({ email: 'not-an-email' })
// validate.errors → [{ instancePath: '/email', keyword: 'format', message: 'must match format "email"' }]| Format name | Example valid value | Notes |
|---|---|---|
email | user@example.com | RFC 5321 simplified; use idn-email for Unicode domains |
uri | https://example.com/path?q=1 | Full URI; uri-reference allows relative paths |
date | 2026-05-12 | ISO 8601 date only (YYYY-MM-DD) |
date-time | 2026-05-12T10:00:00Z | ISO 8601 date + time with timezone |
uuid | 550e8400-e29b-41d4-a716-446655440000 | RFC 4122 UUID v1–v5 |
ipv4 | 192.168.1.1 | Dotted decimal; ipv6 also available |
hostname | api.example.com | RFC 1034 hostname |
json-pointer | /foo/0/bar | RFC 6901 JSON Pointer |
To add only specific formats (for smaller bundles), pass a list: addFormats(ajv, ["email", "uri", "date"]). You can also register a custom format with a regex: ajv.addFormat("phone", /^\+?[0-9]{7,15}$/).
TypeScript type inference with JSONSchemaType and JTD
Ajv v8 ships complete TypeScript types and two modes for schema-driven type inference. Both let TypeScript narrow the validated value's type without a manual type assertion.
JSON Schema mode with JSONSchemaType
Annotate your schema as JSONSchemaType<T> — TypeScript will error at compile time if the schema doesn't match the interface. After validate(data) returns true, TypeScript automatically narrows data to type T:
import Ajv, { JSONSchemaType } from 'ajv'
interface Product {
id: number
name: string
price: number
tags?: string[]
}
const ajv = new Ajv()
const schema: JSONSchemaType<Product> = {
type: 'object',
properties: {
id: { type: 'number' },
name: { type: 'string' },
price: { type: 'number', minimum: 0 },
tags: { type: 'array', items: { type: 'string' }, nullable: true },
},
required: ['id', 'name', 'price'],
}
// TypeScript error if schema doesn't match Product interface
const validate = ajv.compile<Product>(schema)
function processProduct(input: unknown): Product {
if (!validate(input)) {
throw new Error(ajv.errorsText(validate.errors))
}
return input // TypeScript knows input is Product here
}JTD mode for simpler schemas
JSON Type Definition (JTD, RFC 8927) is a leaner alternative to JSON Schema — fewer keywords, unambiguous type mapping, and better code-generation support. Import from ajv/dist/jtd and use JTDSchemaType<T>:
import Ajv, { JTDSchemaType } from 'ajv/dist/jtd'
interface User { id: string; age: number; verified: boolean }
const ajv = new Ajv()
const schema: JTDSchemaType<User> = {
properties: {
id: { type: 'string' },
age: { type: 'uint32' },
verified: { type: 'boolean' },
},
}
const validate = ajv.compile<User>(schema)
// JTD schemas are ~2× faster to compile than equivalent JSON SchemaJTD supports 8 scalar types (boolean, string, timestamp, float32, float64, int8/16/32, uint8/16/32) and 6 forms (properties, optionalProperties, discriminator, elements, values, enum). It does not support minimum, pattern, or other JSON Schema constraints — use JSON Schema when you need those.
Async validation and custom keywords
Ajv supports async validation for rules that require I/O — for example, checking that an email is not already registered in a database. Mark the schema with $async: true and use ajv.compileAsync() instead of ajv.compile(). The returned function returns a Promise.
import Ajv from 'ajv'
const ajv = new Ajv()
// Async keyword: returns true/false from a Promise
ajv.addKeyword({
keyword: 'uniqueEmail',
async: true,
schema: false, // keyword takes no schema value — it's a flag
validate: async function checkEmail(_schema: unknown, email: string) {
const existing = await db.users.findOne({ email })
return existing === null // valid only if no existing user
},
})
const schema = {
$async: true,
type: 'object',
properties: {
email: { type: 'string', format: 'email', uniqueEmail: true },
},
required: ['email'],
}
const validateAsync = ajv.compile(schema)
// Usage: returns Promise<data> on success, throws AjvError on failure
async function registerUser(body: unknown) {
try {
const data = await validateAsync(body)
// data is validated here
} catch (err) {
if (err instanceof Ajv.ValidationError) {
console.error(err.errors) // same error format as sync
}
throw err
}
}Custom synchronous keywords work similarly — omit async: true and return a boolean directly. Custom keywords are compiled into the generated function just like built-in ones, so they run at the same speed once compiled. 3 common use cases for custom keywords: cross-field validation (password must equal confirmPassword), business-rule checks (discount cannot exceed price), and environment-specific constraints (feature flags).
// Synchronous cross-field custom keyword
ajv.addKeyword({
keyword: 'passwordsMatch',
schema: false,
validate: function(_schema: unknown, data: { password: string; confirmPassword: string }) {
return data.password === data.confirmPassword
},
errors: false,
})
const registrationSchema = {
type: 'object',
properties: {
password: { type: 'string', minLength: 8 },
confirmPassword: { type: 'string' },
},
required: ['password', 'confirmPassword'],
passwordsMatch: true,
}
const validateRegistration = ajv.compile(registrationSchema)Draft 2020-12 and strict mode tips
Ajv v8 ships 3 entry points: the default export for Draft-07/2019-09, and dedicated exports for Draft 2020-12 and JTD. Use the correct import to match your $schema declaration — mixing them silently ignores unsupported keywords.
// Draft 2020-12 — supports prefixItems, unevaluatedItems, unevaluatedProperties
import Ajv2020 from 'ajv/dist/2020'
import addFormats from 'ajv-formats'
const ajv = new Ajv2020({ allErrors: true })
addFormats(ajv)
// Draft 2020-12 tuple validation: prefixItems replaces items for positional schemas
const coordinateSchema = {
$schema: 'https://json-schema.org/draft/2020-12/schema',
type: 'array',
prefixItems: [
{ type: 'number', minimum: -180, maximum: 180 }, // longitude
{ type: 'number', minimum: -90, maximum: 90 }, // latitude
],
items: false, // no additional items allowed
minItems: 2,
maxItems: 2,
}
const validateCoord = ajv.compile(coordinateSchema)
console.log(validateCoord([120.5, 35.2])) // true
console.log(validateCoord([200, 35.2])) // false — longitude out of rangeStrict mode (enabled by default in v8) catches 4 common schema authoring mistakes: unknown keywords (e.g. typo minimun instead of minimum), required properties not listed in properties, properties listed without additionalProperties: false when it is likely intended, and boolean schemas in unexpected positions. If you are migrating a Draft-04 schema and need to suppress strict errors temporarily, pass { strict: false }. Fix the underlying issues rather than disabling strict mode in production — strict mode catches real bugs.
See the JSON Schema Draft 2020-12 guide for a full breakdown of prefixItems, unevaluatedProperties, and the updated $ref semantics.
Frequently asked questions
What is Ajv and why is it the fastest JSON Schema validator?
Ajv (Another JSON Validator) is a JavaScript library that validates data against JSON Schema. It is the fastest JSON Schema validator because it compiles schemas into optimized JavaScript functions at startup — rather than interpreting the schema on every call. Benchmarks show 90,000–170,000 validations per second compared to 10,000–30,000 for interpret-based validators. Once compiled, the generated validate function is essentially a tight if/else chain with no schema-parsing overhead. Ajv v8 supports JSON Schema Draft-07 and Draft 2020-12, as well as JSON Type Definition (RFC 8927).
How do I compile and reuse an Ajv schema for best performance?
Call ajv.compile(schema) once when your application starts — at module load time, in a constructor, or in a startup function — and store the returned validate function. Reuse that function on every incoming request or piece of data. Calling ajv.compile() on every request is the most common Ajv performance mistake; compilation takes 10–100 ms depending on schema complexity. At 100 req/sec that costs 1–10 seconds of CPU per second just for validation overhead. The compiled validate function is safe to call concurrently from multiple async operations — there is no shared mutable state.
How do I read and display Ajv validation error messages?
When validation fails, validate.errors is an array of error objects. Each error has 5 fields: instancePath (JSON Pointer like /address/city pointing to the failing field in the data — empty string is the root object), schemaPath (pointer into the schema), keyword (which constraint failed: required, type, minLength, etc.), message (human-readable string), and params (structured detail — e.g. { missingProperty: "email" } for required errors). For a single concatenated string, call ajv.errorsText(validate.errors). To map errors to form fields, strip the leading / from instancePath to get the field name.
How do I add email, date, and URL format validation with Ajv?
Install ajv-formats separately (npm install ajv-formats), then call addFormats(ajv) after creating your Ajv instance and before any ajv.compile() call. This adds 30+ formats: email, uri, date, date-time, uuid, ipv4, ipv6, hostname, json-pointer, and more. By default Ajv v8 ignores the format keyword even when specified — without ajv-formats, "format": "email" passes every string silently. To add only specific formats for a smaller bundle, use addFormats(ajv, ["email", "uri"]). For custom formats, call ajv.addFormat("phone", /pattern/).
How do I use Ajv with TypeScript for type-safe JSON validation?
Ajv v8 ships full TypeScript types. For JSON Schema, annotate your schema variable as JSONSchemaType<T> — TypeScript will report a compile-time error if the schema is inconsistent with your interface. Pass the type parameter to compile: ajv.compile<T>(schema). The returned validate function is a TypeScript type guard: after if (validate(data)), the compiler narrows data to T without a cast. For simpler schemas, use JTD mode (import from ajv/dist/jtd) with JTDSchemaType<T> — JTD maps 1:1 to TypeScript types and compiles ~2× faster than equivalent JSON Schema. See the validate JSON Schema in JavaScript guide for more validator comparisons.
Can Ajv validate JSON Schema Draft 2020-12?
Yes. Ajv v8 ships a separate entry point: import Ajv2020 from "ajv/dist/2020". Draft 2020-12 adds 4 major features over Draft-07: prefixItems (replaces items for positional tuple validation), unevaluatedItems and unevaluatedProperties (catch properties/items not covered by any subschema), and a revised $ref that can be combined with sibling keywords. If your schemas declare $schema: "https://json-schema.org/draft/2020-12/schema", use the 2020 import — the default Ajv import targets Draft-07 and will silently ignore prefixItems. Read the full Draft 2020-12 guide for a keyword-by-keyword comparison.
Validate your JSON Schema with Ajv
Paste your JSON Schema and sample data into Jsonic's JSON Schema Validator — powered by Ajv — to see exactly which constraints fail and where in the data they point.
Validate JSON Schema in Jsonic