Ajv

バリデーションライブラリJavaScriptJSON SchemaTypeScriptランタイムパフォーマンス

GitHub概要

ajv-validator/ajv

The fastest JSON schema Validator. Supports JSON Schema draft-04/06/07/2019-09/2020-12 and JSON Type Definition (RFC8927)

スター14,418
ウォッチ109
フォーク930
作成日:2015年5月19日
言語:TypeScript
ライセンス:MIT License

トピックス

ajvjson-schemavalidator

スター履歴

ajv-validator/ajv Star History
データ取得日時: 2025/10/22 09:49

ライブラリ

Ajv

概要

Ajvは「Another JSON Schema validator」として知られる、世界最速のJSON Schemaバリデーションライブラリです。JSON Schema draft-04/06/07/2019-09/2020-12およびJSON Type Definition (RFC8927)をサポートし、Node.jsとブラウザの両方で動作します。高いパフォーマンスとコンパイル時最適化を特徴とし、2025年現在もJavaScriptエコシステムにおけるJSON Schemaバリデーションのデファクトスタンダードです。

詳細

Ajv 9は2025年現在の最新安定版で、JSON Schemaの全バージョンを包括的にサポートしています。「A」はAnother、「j」はJSON、「v」はvalidatorを表し、シンプルな名前でありながら業界標準の機能を提供します。12k+のGitHubスターを獲得し、express-validator、fastify、JSON Schema Editorなど多くのライブラリで内部的に使用されています。バリデーション実行時のパフォーマンスを最優先に設計され、コンパイル済みバリデーション関数による高速実行を実現。

主な特徴

  • 最速のパフォーマンス: コンパイル済みバリデーション関数による最適化実行
  • 完全なJSON Schema対応: 全ドラフトバージョンと最新仕様への準拠
  • ゼロ依存関係: 外部ライブラリに依存しない軽量設計
  • TypeScript完全対応: 型安全なバリデーションとコンパイル時チェック
  • カスタムキーワード: 独自のバリデーションルールの拡張可能
  • 詳細なエラー情報: デバッグに有用な包括的エラーレポート

メリット・デメリット

メリット

  • 業界最高レベルのバリデーション速度
  • JSON Schema標準への完全準拠で相互運用性が高い
  • 豊富なコミュニティとエコシステム
  • TypeScriptとの優れた統合
  • 多様な実行環境(Node.js、ブラウザ、CDN)対応
  • 詳細で理解しやすいエラーメッセージ

デメリット

  • 学習コストがやや高い(JSON Schema仕様の理解が必要)
  • 複雑なスキーマでの設定時間
  • カスタムキーワード定義の複雑性
  • 大規模スキーマでのメモリ使用量
  • 非JSON形式データには適用できない
  • TypeScript型推論は別ライブラリが必要

参考ページ

書き方の例

インストールと基本セットアップ

# Ajvのインストール
npm install ajv
yarn add ajv
pnpm add ajv
bun add ajv

# TypeScript定義も含まれています
# 追加のformat対応(オプション)
npm install ajv-formats

基本的なスキーマ定義とバリデーション

const Ajv = require("ajv");
const ajv = new Ajv(); // オプション: {allErrors: true}

// 基本的なJSON Schemaの定義
const schema = {
  type: "object",
  properties: {
    name: { type: "string" },
    age: { type: "number", minimum: 0 },
    email: { type: "string", format: "email" }
  },
  required: ["name", "age"],
  additionalProperties: false
};

// スキーマのコンパイル
const validate = ajv.compile(schema);

// データのバリデーション
const userData = { name: "田中太郎", age: 30, email: "[email protected]" };

const valid = validate(userData);
if (valid) {
  console.log("バリデーション成功");
} else {
  console.log("バリデーションエラー:", validate.errors);
  /*
  [
    {
      instancePath: "/email",
      schemaPath: "#/properties/email/format",
      keyword: "format",
      params: { format: "email" },
      message: "must match format \"email\""
    }
  ]
  */
}

// 型の種類
const stringSchema = { type: "string" };
const numberSchema = { type: "number" };
const booleanSchema = { type: "boolean" };
const arraySchema = { type: "array", items: { type: "string" } };
const objectSchema = { type: "object" };

// バリデーション実行
stringSchema.parse = ajv.compile(stringSchema);
console.log(stringSchema.parse("Hello")); // true
console.log(stringSchema.parse(123)); // false

高度なバリデーションルールとカスタムバリデーター

// 数値の範囲指定
const ageSchema = {
  type: "number",
  minimum: 0,
  maximum: 150,
  multipleOf: 1 // 整数のみ
};

// 文字列の詳細バリデーション
const passwordSchema = {
  type: "string",
  minLength: 8,
  maxLength: 50,
  pattern: "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)" // 大文字、小文字、数字を含む
};

// 配列とネストオブジェクト
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
};

// 条件付きバリデーション(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"] }
  }
};

// カスタムキーワードの定義
ajv.addKeyword({
  keyword: "even",
  type: "number",
  schemaType: "boolean",
  compile: function(schemaVal) {
    return function validate(data) {
      if (schemaVal) {
        return data % 2 === 0;
      }
      return true;
    };
  }
});

// カスタムキーワードの使用
const evenNumberSchema = {
  type: "number",
  even: true
};

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

フレームワーク統合(Express、Fastify、NestJS等)

// Express.jsでのミドルウェア例
const express = require('express');
const Ajv = require('ajv');
const addFormats = require('ajv-formats');

const ajv = new Ajv();
addFormats(ajv); // 標準フォーマット(email、uri等)を追加

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

// バリデーションミドルウェア
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: "バリデーションエラー",
        details: validate.errors
      });
    }
    next();
  };
};

// APIエンドポイントでの使用
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は型安全にアクセス可能
  res.json({ message: "ユーザー作成成功", user: req.body });
});

// Fastifyでの統合例
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は自動的にバリデーションされる
  return { id: 'generated-id', ...request.body };
});

// NestJSでのValidationPipeとの併用
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: 'バリデーションエラー',
        errors: validate.errors
      });
    }
    
    return value;
  }
}

エラーハンドリングとカスタムエラーメッセージ

// 詳細なエラーハンドリング
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("エラー詳細:", 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"
    }
  ]
  */
}

// カスタムエラーメッセージ
const schemaWithCustomMessages = {
  type: "object",
  properties: {
    email: {
      type: "string",
      format: "email",
      errorMessage: "有効なメールアドレスを入力してください"
    },
    password: {
      type: "string",
      minLength: 8,
      errorMessage: {
        minLength: "パスワードは8文字以上である必要があります"
      }
    }
  },
  required: ["email", "password"],
  errorMessage: {
    required: {
      email: "メールアドレスは必須です",
      password: "パスワードは必須です"
    }
  }
};

// エラーメッセージプラグインの使用
const ajvErrors = require('ajv-errors');
ajvErrors(ajv);

// エラー情報の整理
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;
}

// 使用例
const userData = { email: "invalid-email", password: "123" };
const isValid = validate(userData);

if (!isValid) {
  const fieldErrors = formatErrors(validate.errors);
  console.log(fieldErrors);
  // { email: "有効なメールアドレスを入力してください", password: "パスワードは8文字以上である必要があります" }
}

TypeScript統合と型安全性

import Ajv, { JSONSchemaType } from 'ajv';

const ajv = new Ajv();

// TypeScript インターフェースの定義
interface User {
  id: string;
  name: string;
  email: string;
  age: number;
  active?: boolean;
}

// TypeScript型に対応したJSON Schema
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
};

// 型安全なバリデーション関数
const validateUser = ajv.compile(userSchema);

// 型ガード関数として使用
function isValidUser(data: unknown): data is User {
  return validateUser(data);
}

// 使用例
const userData: unknown = {
  id: "12345",
  name: "田中太郎",
  email: "[email protected]",
  age: 30
};

if (isValidUser(userData)) {
  // ここでuserDataはUser型として扱われる
  console.log(`ユーザー名: ${userData.name}`);
  console.log(`年齢: ${userData.age}`);
} else {
  console.log("無効なユーザーデータ:", validateUser.errors);
}

// 複雑な型の例
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
});

// ジェネリック型での使用
const userApiResponseSchema = createApiResponseSchema(userSchema);
const validateUserApiResponse = ajv.compile(userApiResponseSchema);

// 外部APIレスポンスの検証
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; // 型安全にApiResponse<User>として返す
  }
  
  console.error("APIレスポンスのバリデーションエラー:", validateUserApiResponse.errors);
  return null;
}

パフォーマンス最適化とベンチマーク

// スキーマのプリコンパイル(推奨)
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"]
  })
};

// 大量データの高速バリデーション
function validateBulkData(items, validator) {
  const startTime = performance.now();
  const results = items.map(item => validator(item));
  const endTime = performance.now();
  
  console.log(`${items.length}件のバリデーション時間: ${endTime - startTime}ms`);
  return results;
}

// メモリ効率的なバリデーション
const ajvFast = new Ajv({
  code: { optimize: true },
  allErrors: false, // 最初のエラーで停止(高速化)
  verbose: false,   // 詳細情報を削減
  strict: false     // 厳密モードを無効(高速化)
});

// ストリームデータのバリデーション
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(`バリデーションエラー: ${JSON.stringify(this.validate.errors)}`));
    }
    callback();
  }
}

// 使用例
const validationStream = new ValidationStream(userSchema);
validationStream.on('error', (error) => {
  console.error('ストリームバリデーションエラー:', error.message);
});