AJV

TypeScriptValidationJSON SchemaPerformanceAsync Validation

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は最速のJSON Schemaバリデーターライブラリです。JSON Schema draft-04/06/07/2019-09/2020-12とJSON Type Definition (RFC8927)をサポートし、高いパフォーマンスと豊富な機能を提供します。JSONスキーマ標準に準拠した堅牢なバリデーションシステムを構築できるため、企業向けアプリケーションやAPIサーバーで広く採用されています。

特徴

  • 高速パフォーマンス: 業界最高速のJSON Schemaバリデーター
  • 標準準拠: JSON Schema最新仕様完全対応
  • TypeScript統合: 優れた型安全性とIntelliSense支援
  • 非同期バリデーション: データベース検索やAPI呼び出しを含む検証
  • カスタムキーワード: 独自バリデーションルールの定義
  • カスタムフォーマット: 特殊な文字列フォーマットの検証
  • エラーレポート: 詳細なエラー情報と位置特定
  • コード生成: バリデーション関数の事前コンパイル
  • JTDサポート: JSON Type Definition仕様対応
  • 軽量: 最小限の依存関係とコンパクトなサイズ

インストール

npm install ajv
# または
yarn add ajv
# または
pnpm add ajv
# または
bun add ajv

# TypeScript用の型定義(既に含まれています)
# 追加のフォーマット検証が必要な場合
npm install ajv-formats

使用例

基本的な使用方法

import Ajv from 'ajv';

// AJVインスタンスの作成
const ajv = new Ajv();

// JSON Schemaの定義
const schema = {
  type: "object",
  properties: {
    name: { type: "string" },
    age: { type: "number", minimum: 0, maximum: 150 },
    email: { type: "string", format: "email" },
    isActive: { type: "boolean", default: true }
  },
  required: ["name", "age", "email"],
  additionalProperties: false
};

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

// データの検証
const userData = {
  name: "山田太郎",
  age: 30,
  email: "[email protected]"
};

if (validate(userData)) {
  console.log("検証成功:", userData);
} else {
  console.log("検証エラー:", validate.errors);
}

// TypeScriptでの型安全な検証
interface User {
  name: string;
  age: number;
  email: string;
  isActive?: boolean;
}

const validateUser = ajv.compile<User>(schema);

function processUser(data: unknown): User | null {
  if (validateUser(data)) {
    // dataはUser型として型推論される
    return data;
  }
  console.error("User validation failed:", validateUser.errors);
  return null;
}

JSON Schemaの高度な機能

// 複雑なスキーマの定義
const productSchema = {
  type: "object",
  properties: {
    id: { type: "string", pattern: "^[A-Z]{2}[0-9]{4}$" },
    name: { type: "string", minLength: 1, maxLength: 100 },
    price: { type: "number", minimum: 0, multipleOf: 0.01 },
    category: { enum: ["electronics", "clothing", "books", "home"] },
    tags: {
      type: "array",
      items: { type: "string" },
      minItems: 1,
      maxItems: 10,
      uniqueItems: true
    },
    variants: {
      type: "array",
      items: {
        type: "object",
        properties: {
          size: { type: "string" },
          color: { type: "string" },
          stock: { type: "integer", minimum: 0 }
        },
        required: ["size", "color", "stock"]
      }
    },
    metadata: {
      type: "object",
      patternProperties: {
        "^[a-z_]+$": { type: "string" }
      },
      additionalProperties: false
    }
  },
  required: ["id", "name", "price", "category"],
  additionalProperties: false
};

// 条件付きスキーマ
const orderSchema = {
  type: "object",
  properties: {
    type: { enum: ["physical", "digital"] },
    amount: { type: "number", minimum: 0 },
    shippingAddress: { type: "string" },
    downloadLink: { type: "string", format: "uri" }
  },
  required: ["type", "amount"],
  if: { properties: { type: { const: "physical" } } },
  then: { required: ["shippingAddress"] },
  else: { required: ["downloadLink"] }
};

// スキーマの参照と再利用
const addressSchema = {
  $id: "https://example.com/address.json",
  type: "object",
  properties: {
    street: { type: "string" },
    city: { type: "string" },
    zipCode: { type: "string", pattern: "^[0-9]{5}(-[0-9]{4})?$" }
  },
  required: ["street", "city", "zipCode"]
};

const customerSchema = {
  type: "object",
  properties: {
    name: { type: "string" },
    billingAddress: { $ref: "https://example.com/address.json" },
    shippingAddress: { $ref: "https://example.com/address.json" }
  },
  required: ["name", "billingAddress"]
};

// スキーマの登録と使用
ajv.addSchema(addressSchema);
const validateCustomer = ajv.compile(customerSchema);

カスタムキーワードとフォーマット

import addFormats from 'ajv-formats';

// フォーマット検証の追加
const ajv = new Ajv();
addFormats(ajv);

// カスタムキーワードの定義
ajv.addKeyword({
  keyword: "range",
  type: "number",
  schemaType: "array",
  compile(schemaValue: [number, number]) {
    const [min, max] = schemaValue;
    return function validate(data: number): boolean {
      return data >= min && data <= max;
    };
  }
});

// カスタムフォーマットの定義
ajv.addFormat("japanese-phone", {
  type: "string",
  validate: function(data: string): boolean {
    // 日本の電話番号形式をチェック
    return /^0\d{1,4}-\d{1,4}-\d{4}$/.test(data);
  }
});

// カスタムキーワードとフォーマットの使用
const schema = {
  type: "object",
  properties: {
    score: { type: "number", range: [0, 100] },
    phone: { type: "string", format: "japanese-phone" },
    email: { type: "string", format: "email" },
    date: { type: "string", format: "date" }
  }
};

// より高度なカスタムキーワード(非同期対応)
ajv.addKeyword({
  keyword: "uniqueEmail",
  type: "string",
  format: "email",
  async: true,
  compile() {
    return async function validate(email: string): Promise<boolean> {
      // データベースでメールアドレスの重複をチェック
      // const exists = await checkEmailExists(email);
      // return !exists;
      
      // 模擬的な非同期チェック
      return new Promise((resolve) => {
        setTimeout(() => {
          resolve(!email.includes("taken"));
        }, 100);
      });
    };
  }
});

非同期バリデーション

// 非同期バリデーションスキーマ
const asyncSchema = {
  type: "object",
  properties: {
    username: { 
      type: "string", 
      minLength: 3,
      uniqueUsername: true 
    },
    email: { 
      type: "string", 
      format: "email",
      uniqueEmail: true 
    }
  },
  required: ["username", "email"]
};

// 非同期バリデーション関数
ajv.addKeyword({
  keyword: "uniqueUsername",
  type: "string",
  async: true,
  compile() {
    return async function validate(username: string): Promise<boolean> {
      // データベースでユーザー名の重複をチェック
      console.log(`Checking username: ${username}`);
      await new Promise(resolve => setTimeout(resolve, 200));
      return !["admin", "root", "system"].includes(username.toLowerCase());
    };
  }
});

// 非同期バリデーションの実行
const validateAsync = ajv.compile(asyncSchema);

async function validateUserData(data: unknown) {
  try {
    const valid = await validateAsync(data);
    if (valid) {
      console.log("非同期検証成功:", data);
      return data;
    } else {
      console.error("非同期検証エラー:", validateAsync.errors);
      return null;
    }
  } catch (error) {
    console.error("非同期検証で例外発生:", error);
    return null;
  }
}

// 使用例
validateUserData({
  username: "newuser",
  email: "[email protected]"
});

エラーハンドリングとカスタマイズ

// 詳細なエラー情報の取得
const ajv = new Ajv({ 
  allErrors: true,      // すべてのエラーを収集
  verbose: true,        // 詳細な情報を含める
  $data: true,          // $dataリファレンスを有効化
  removeAdditional: true // 追加プロパティを自動削除
});

const schema = {
  type: "object",
  properties: {
    name: { type: "string", minLength: 2 },
    age: { type: "number", minimum: 0 },
    email: { type: "string", format: "email" }
  },
  required: ["name", "age"]
};

const validate = ajv.compile(schema);

function validateWithDetailedErrors(data: unknown) {
  const valid = validate(data);
  
  if (!valid && validate.errors) {
    const errorMessages = validate.errors.map(error => {
      const { instancePath, keyword, message, params } = error;
      return {
        field: instancePath || "root",
        rule: keyword,
        message: message,
        rejectedValue: error.data,
        params: params
      };
    });
    
    console.error("バリデーションエラー詳細:", errorMessages);
    return { success: false, errors: errorMessages };
  }
  
  return { success: true, data };
}

// カスタムエラーメッセージ
const customErrorSchema = {
  type: "object",
  properties: {
    username: { 
      type: "string", 
      minLength: 3,
      errorMessage: {
        type: "ユーザー名は文字列である必要があります",
        minLength: "ユーザー名は3文字以上で入力してください"
      }
    },
    password: { 
      type: "string", 
      minLength: 8,
      pattern: "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d).+$",
      errorMessage: {
        minLength: "パスワードは8文字以上必要です",
        pattern: "パスワードには大文字、小文字、数字を含める必要があります"
      }
    }
  },
  required: ["username", "password"],
  errorMessage: {
    required: {
      username: "ユーザー名は必須項目です",
      password: "パスワードは必須項目です"
    }
  }
};

// ajv-errorsプラグインを使用
import ajvErrors from 'ajv-errors';
ajvErrors(ajv);

パフォーマンス最適化

// コード生成による最適化
const ajv = new Ajv({ 
  code: { 
    optimize: true,     // コード最適化を有効化
    regExp: false       // 正規表現の最適化を無効化(互換性のため)
  }
});

// スタンドアロン関数の生成
import standaloneCode from 'ajv/dist/standalone';

const schema = {
  type: "object",
  properties: {
    id: { type: "number" },
    name: { type: "string" }
  },
  required: ["id", "name"]
};

const validate = ajv.compile(schema);
const moduleCode = standaloneCode(ajv, validate);

// 生成されたコードをファイルに保存
// このコードは依存関係なしで実行可能
console.log(moduleCode);

// スキーマキャッシュの活用
const schemaCache = new Map();

function getCachedValidator(schemaId: string, schema: object) {
  if (schemaCache.has(schemaId)) {
    return schemaCache.get(schemaId);
  }
  
  const validator = ajv.compile(schema);
  schemaCache.set(schemaId, validator);
  return validator;
}

// バッチバリデーション
function validateBatch<T>(validator: any, dataArray: unknown[]): T[] {
  const results: T[] = [];
  
  for (const data of dataArray) {
    if (validator(data)) {
      results.push(data as T);
    } else {
      console.warn("Invalid data skipped:", validator.errors);
    }
  }
  
  return results;
}

API統合とミドルウェア

// Express.jsミドルウェア
import express from 'express';

interface ValidationSchema {
  body?: object;
  query?: object;
  params?: object;
}

function createValidator(schemas: ValidationSchema) {
  const validators = {
    body: schemas.body ? ajv.compile(schemas.body) : null,
    query: schemas.query ? ajv.compile(schemas.query) : null,
    params: schemas.params ? ajv.compile(schemas.params) : null
  };

  return (req: express.Request, res: express.Response, next: express.NextFunction) => {
    const errors: any[] = [];

    // リクエストボディの検証
    if (validators.body && !validators.body(req.body)) {
      errors.push({
        location: "body",
        errors: validators.body.errors
      });
    }

    // クエリパラメータの検証
    if (validators.query && !validators.query(req.query)) {
      errors.push({
        location: "query",
        errors: validators.query.errors
      });
    }

    // パスパラメータの検証
    if (validators.params && !validators.params(req.params)) {
      errors.push({
        location: "params",
        errors: validators.params.errors
      });
    }

    if (errors.length > 0) {
      return res.status(400).json({
        error: "バリデーションエラー",
        details: errors
      });
    }

    next();
  };
}

// 使用例
const app = express();
app.use(express.json());

const userCreateSchema = {
  body: {
    type: "object",
    properties: {
      name: { type: "string", minLength: 1 },
      email: { type: "string", format: "email" },
      age: { type: "number", minimum: 0 }
    },
    required: ["name", "email"],
    additionalProperties: false
  }
};

app.post('/users', 
  createValidator(userCreateSchema),
  (req: express.Request, res: express.Response) => {
    // req.bodyは検証済み
    const userData = req.body;
    console.log("新しいユーザー:", userData);
    res.json({ success: true, user: userData });
  }
);

// Fastifyプラグインとしての使用
import fastify from 'fastify';

const server = fastify();

server.addHook('preHandler', async (request, reply) => {
  if (request.routerMethod === 'POST' && request.url === '/api/users') {
    const userSchema = {
      type: "object",
      properties: {
        name: { type: "string" },
        email: { type: "string", format: "email" }
      },
      required: ["name", "email"]
    };

    const validate = ajv.compile(userSchema);
    if (!validate(request.body)) {
      reply.code(400).send({
        error: "Validation failed",
        details: validate.errors
      });
    }
  }
});

JSON Type Definition (JTD)の使用

import Ajv from 'ajv/dist/jtd';

// JTD専用のAJVインスタンス
const ajvJTD = new Ajv();

// JTDスキーマの定義
const userSchemaJTD = {
  properties: {
    name: { type: "string" },
    age: { type: "uint16" },
    email: { type: "string" }
  },
  optionalProperties: {
    isActive: { type: "boolean" }
  },
  additionalProperties: false
};

// TypeScript型の生成
import { JTDSchemaType } from 'ajv/dist/jtd';

interface User {
  name: string;
  age: number;
  email: string;
  isActive?: boolean;
}

const schema: JTDSchemaType<User> = {
  properties: {
    name: { type: "string" },
    age: { type: "uint16" },
    email: { type: "string" }
  },
  optionalProperties: {
    isActive: { type: "boolean" }
  }
};

const validateJTD = ajvJTD.compile(schema);

// 型安全なバリデーション
function processUserJTD(data: unknown): User | null {
  if (validateJTD(data)) {
    // dataは自動的にUser型として推論される
    return data;
  }
  return null;
}

比較・代替手段

類似ライブラリとの比較

  • Zod: TypeScript-firstで優れたDX、but JSON Schema標準非対応
  • Yup: 使いやすいAPI、but パフォーマンスが劣る
  • Joi: 豊富な機能、but ブラウザでのサイズが大きい
  • io-ts: 関数型アプローチ、but 学習コストが高い
  • JSON Schema libraries: 他の実装よりもAJVが最速

AJVを選ぶべき場合

  • JSON Schema標準準拠が必要
  • 高いパフォーマンスが求められる
  • 大量のデータ検証が必要
  • 既存のJSON Schemaアセットを活用したい
  • 非同期バリデーションが必要
  • エンタープライズレベルの堅牢性が必要

学習リソース

まとめ

AJVは業界標準のJSON Schemaバリデーターとして、高いパフォーマンスと豊富な機能を提供します。特に、大規模なAPIサーバーやエンタープライズアプリケーションにおいて、データの整合性とパフォーマンスの両方が求められる場面で威力を発揮します。JSON Schema標準への準拠により、他のシステムとの相互運用性も確保でき、長期的なメンテナンス性にも優れています。TypeScriptとの統合により、型安全性を保ちながら実行時バリデーションを実現できる点も大きな魅力です。