Ajv

Validation LibraryJavaScriptJSON SchemaTypeScriptRuntimePerformance

Library

Ajv

Overview

Ajv stands for "Another JSON Schema validator" and is the world's fastest JSON Schema validation library. It supports JSON Schema draft-04/06/07/2019-09/2020-12 and JSON Type Definition (RFC8927), working in both Node.js and browsers. Featuring high performance and compile-time optimization, Ajv remains the de facto standard for JSON Schema validation in the JavaScript ecosystem as of 2025.

Details

Ajv 9 is the latest stable version as of 2025, providing comprehensive support for all JSON Schema versions. The name stands for "A"nother "j"SON "v"alidator, delivering industry-standard functionality with a simple name. With 12k+ GitHub stars, Ajv is internally used by many libraries including express-validator, fastify, and JSON Schema Editor. Designed with validation runtime performance as the top priority, it achieves high-speed execution through compiled validation functions.

Key Features

  • Fastest Performance: Optimized execution through compiled validation functions
  • Complete JSON Schema Support: Compliance with all draft versions and latest specifications
  • Zero Dependencies: Lightweight design without external library dependencies
  • Full TypeScript Support: Type-safe validation and compile-time checking
  • Custom Keywords: Extensible with custom validation rules
  • Detailed Error Information: Comprehensive error reports useful for debugging

Pros and Cons

Pros

  • Industry-leading validation performance
  • High interoperability with complete JSON Schema standard compliance
  • Rich community and ecosystem
  • Excellent TypeScript integration
  • Multi-environment support (Node.js, browsers, CDN)
  • Detailed and understandable error messages

Cons

  • Somewhat high learning curve (requires understanding of JSON Schema specifications)
  • Setup time for complex schemas
  • Complexity in defining custom keywords
  • Memory usage with large schemas
  • Not applicable to non-JSON format data
  • Requires separate library for TypeScript type inference

Reference Pages

Code Examples

Installation and Basic Setup

# Installing Ajv
npm install ajv
yarn add ajv
pnpm add ajv
bun add ajv

# TypeScript definitions included
# Additional format support (optional)
npm install ajv-formats

Basic Schema Definition and Validation

const Ajv = require("ajv");
const ajv = new Ajv(); // options: {allErrors: true}

// Basic JSON Schema definition
const schema = {
  type: "object",
  properties: {
    name: { type: "string" },
    age: { type: "number", minimum: 0 },
    email: { type: "string", format: "email" }
  },
  required: ["name", "age"],
  additionalProperties: false
};

// Schema compilation
const validate = ajv.compile(schema);

// Data validation
const userData = { name: "John Doe", age: 30, email: "[email protected]" };

const valid = validate(userData);
if (valid) {
  console.log("Validation successful");
} else {
  console.log("Validation errors:", validate.errors);
  /*
  [
    {
      instancePath: "/email",
      schemaPath: "#/properties/email/format",
      keyword: "format",
      params: { format: "email" },
      message: "must match format \"email\""
    }
  ]
  */
}

// Type examples
const stringSchema = { type: "string" };
const numberSchema = { type: "number" };
const booleanSchema = { type: "boolean" };
const arraySchema = { type: "array", items: { type: "string" } };
const objectSchema = { type: "object" };

// Validation execution
stringSchema.parse = ajv.compile(stringSchema);
console.log(stringSchema.parse("Hello")); // true
console.log(stringSchema.parse(123)); // false

Advanced Validation Rules and Custom Validators

// Number range specification
const ageSchema = {
  type: "number",
  minimum: 0,
  maximum: 150,
  multipleOf: 1 // integers only
};

// String detailed validation
const passwordSchema = {
  type: "string",
  minLength: 8,
  maxLength: 50,
  pattern: "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)" // contains uppercase, lowercase, and digits
};

// Arrays and nested objects
const productSchema = {
  type: "object",
  properties: {
    id: { type: "string", format: "uuid" },
    name: { type: "string", minLength: 1 },
    price: { type: "number", minimum: 0 },
    categories: {
      type: "array",
      items: { type: "string" },
      minItems: 1
    },
    metadata: {
      type: "object",
      additionalProperties: { type: "string" }
    }
  },
  required: ["id", "name", "price"],
  additionalProperties: false
};

// Conditional validation (if/then/else)
const userSchema = {
  type: "object",
  properties: {
    role: { type: "string", enum: ["admin", "user", "guest"] },
    permissions: { type: "array", items: { type: "string" } },
    department: { type: "string" }
  },
  if: { properties: { role: { const: "admin" } } },
  then: { required: ["permissions"] },
  else: { 
    if: { properties: { role: { const: "user" } } },
    then: { required: ["department"] }
  }
};

// Custom keyword definition
ajv.addKeyword({
  keyword: "even",
  type: "number",
  schemaType: "boolean",
  compile: function(schemaVal) {
    return function validate(data) {
      if (schemaVal) {
        return data % 2 === 0;
      }
      return true;
    };
  }
});

// Custom keyword usage
const evenNumberSchema = {
  type: "number",
  even: true
};

const validateEven = ajv.compile(evenNumberSchema);
console.log(validateEven(4)); // true
console.log(validateEven(3)); // false

Framework Integration (Express, Fastify, NestJS, etc.)

// Express.js middleware example
const express = require('express');
const Ajv = require('ajv');
const addFormats = require('ajv-formats');

const ajv = new Ajv();
addFormats(ajv); // Add standard formats (email, uri, etc.)

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

// Validation middleware
const validateRequest = (schema) => {
  const validate = ajv.compile(schema);
  return (req, res, next) => {
    const valid = validate(req.body);
    if (!valid) {
      return res.status(400).json({
        error: "Validation error",
        details: validate.errors
      });
    }
    next();
  };
};

// API endpoint usage
const createUserSchema = {
  type: "object",
  properties: {
    name: { type: "string", minLength: 1, maxLength: 100 },
    email: { type: "string", format: "email" },
    age: { type: "integer", minimum: 18, maximum: 120 }
  },
  required: ["name", "email", "age"],
  additionalProperties: false
};

app.post('/users', validateRequest(createUserSchema), (req, res) => {
  // req.body can be accessed in a type-safe manner
  res.json({ message: "User creation successful", user: req.body });
});

// Fastify integration example
const fastify = require('fastify')({ logger: true });

const userRouteSchema = {
  body: {
    type: 'object',
    required: ['name', 'email'],
    properties: {
      name: { type: 'string' },
      email: { type: 'string', format: 'email' }
    }
  },
  response: {
    200: {
      type: 'object',
      properties: {
        id: { type: 'string' },
        name: { type: 'string' },
        email: { type: 'string' }
      }
    }
  }
};

fastify.post('/users', { schema: userRouteSchema }, async (request, reply) => {
  // request is automatically validated
  return { id: 'generated-id', ...request.body };
});

// NestJS ValidationPipe integration
import { Injectable, PipeTransform, BadRequestException } from '@nestjs/common';
import Ajv from 'ajv';

@Injectable()
export class AjvValidationPipe implements PipeTransform {
  private ajv = new Ajv();

  constructor(private schema: any) {}

  transform(value: any) {
    const validate = this.ajv.compile(this.schema);
    const isValid = validate(value);
    
    if (!isValid) {
      throw new BadRequestException({
        message: 'Validation error',
        errors: validate.errors
      });
    }
    
    return value;
  }
}

Error Handling and Custom Error Messages

// Detailed error handling
const schema = {
  type: "object",
  properties: {
    name: { type: "string", minLength: 2 },
    age: { type: "number", minimum: 0 }
  },
  required: ["name", "age"]
};

const validate = ajv.compile(schema);
const result = validate({ name: "A", age: -1 });

if (!result) {
  console.log("Error details:", validate.errors);
  /*
  [
    {
      instancePath: "/name",
      schemaPath: "#/properties/name/minLength",
      keyword: "minLength",
      params: { limit: 2 },
      message: "must NOT have fewer than 2 characters"
    },
    {
      instancePath: "/age", 
      schemaPath: "#/properties/age/minimum",
      keyword: "minimum",
      params: { comparison: ">=", limit: 0 },
      message: "must be >= 0"
    }
  ]
  */
}

// Custom error messages
const schemaWithCustomMessages = {
  type: "object",
  properties: {
    email: {
      type: "string",
      format: "email",
      errorMessage: "Please enter a valid email address"
    },
    password: {
      type: "string",
      minLength: 8,
      errorMessage: {
        minLength: "Password must be at least 8 characters long"
      }
    }
  },
  required: ["email", "password"],
  errorMessage: {
    required: {
      email: "Email is required",
      password: "Password is required"
    }
  }
};

// Error message plugin usage
const ajvErrors = require('ajv-errors');
ajvErrors(ajv);

// Error information organization
function formatErrors(errors) {
  const formattedErrors = {};
  
  errors.forEach(error => {
    const field = error.instancePath.slice(1) || error.params?.missingProperty;
    if (field) {
      formattedErrors[field] = error.message;
    }
  });
  
  return formattedErrors;
}

// Usage example
const userData = { email: "invalid-email", password: "123" };
const isValid = validate(userData);

if (!isValid) {
  const fieldErrors = formatErrors(validate.errors);
  console.log(fieldErrors);
  // { email: "Please enter a valid email address", password: "Password must be at least 8 characters long" }
}

TypeScript Integration and Type Safety

import Ajv, { JSONSchemaType } from 'ajv';

const ajv = new Ajv();

// TypeScript interface definition
interface User {
  id: string;
  name: string;
  email: string;
  age: number;
  active?: boolean;
}

// JSON Schema corresponding to TypeScript type
const userSchema: JSONSchemaType<User> = {
  type: "object",
  properties: {
    id: { type: "string" },
    name: { type: "string" },
    email: { type: "string", format: "email" },
    age: { type: "number", minimum: 0 },
    active: { type: "boolean", nullable: true }
  },
  required: ["id", "name", "email", "age"],
  additionalProperties: false
};

// Type-safe validation function
const validateUser = ajv.compile(userSchema);

// Use as type guard function
function isValidUser(data: unknown): data is User {
  return validateUser(data);
}

// Usage example
const userData: unknown = {
  id: "12345",
  name: "John Doe",
  email: "[email protected]",
  age: 30
};

if (isValidUser(userData)) {
  // Here userData is treated as User type
  console.log(`User name: ${userData.name}`);
  console.log(`Age: ${userData.age}`);
} else {
  console.log("Invalid user data:", validateUser.errors);
}

// Complex type example
interface ApiResponse<T> {
  success: boolean;
  data?: T;
  error?: string;
  timestamp: string;
}

const createApiResponseSchema = <T>(dataSchema: JSONSchemaType<T>): JSONSchemaType<ApiResponse<T>> => ({
  type: "object",
  properties: {
    success: { type: "boolean" },
    data: { ...dataSchema, nullable: true },
    error: { type: "string", nullable: true },
    timestamp: { type: "string", format: "date-time" }
  },
  required: ["success", "timestamp"],
  additionalProperties: false
});

// Generic type usage
const userApiResponseSchema = createApiResponseSchema(userSchema);
const validateUserApiResponse = ajv.compile(userApiResponseSchema);

// External API response validation
async function fetchUser(id: string): Promise<ApiResponse<User> | null> {
  const response = await fetch(`/api/users/${id}`);
  const data = await response.json();
  
  if (validateUserApiResponse(data)) {
    return data; // Type-safely return as ApiResponse<User>
  }
  
  console.error("API response validation error:", validateUserApiResponse.errors);
  return null;
}

Performance Optimization and Benchmarking

// Schema pre-compilation (recommended)
const schemas = {
  user: ajv.compile({
    type: "object",
    properties: {
      name: { type: "string" },
      email: { type: "string", format: "email" }
    },
    required: ["name", "email"]
  }),
  
  product: ajv.compile({
    type: "object", 
    properties: {
      id: { type: "string" },
      price: { type: "number", minimum: 0 }
    },
    required: ["id", "price"]
  })
};

// High-speed validation of large datasets
function validateBulkData(items, validator) {
  const startTime = performance.now();
  const results = items.map(item => validator(item));
  const endTime = performance.now();
  
  console.log(`Validation time for ${items.length} items: ${endTime - startTime}ms`);
  return results;
}

// Memory-efficient validation
const ajvFast = new Ajv({
  code: { optimize: true },
  allErrors: false, // Stop at first error (faster)
  verbose: false,   // Reduce detailed information
  strict: false     // Disable strict mode (faster)
});

// Stream data validation
const stream = require('stream');

class ValidationStream extends stream.Transform {
  constructor(schema) {
    super({ objectMode: true });
    this.validate = ajv.compile(schema);
  }
  
  _transform(chunk, encoding, callback) {
    if (this.validate(chunk)) {
      this.push(chunk);
    } else {
      this.emit('error', new Error(`Validation error: ${JSON.stringify(this.validate.errors)}`));
    }
    callback();
  }
}

// Usage example
const validationStream = new ValidationStream(userSchema);
validationStream.on('error', (error) => {
  console.error('Stream validation error:', error.message);
});