テスト

Ajvとは:JSON SchemaをコンパイルするNode.jsの検証ライブラリの使い方とエラー整形

Ajvとは:JSON SchemaをコンパイルするNode.jsの検証ライブラリの使い方とエラー整形

Ajvは、JSON Schemaで書いた検証ルールをJavaScriptの関数へコンパイルし、その関数でデータを検査するライブラリです。名前はAnother JSON Schema Validatorの略で、2026年11月1日時点の最新版はnpmで8.20.0です。この記事では、npm installからcompileとvalidateの基本、strictモードやallErrorsの既定値、ajv-errorsで日本語メッセージを付ける方法、APIで400を返す実装、standaloneコードとReDoS対策までを順に扱います。最後にZodとの選び分けを条件付きで示します。

まとめ:Ajvは起動時に1回コンパイルし本番ではallErrorsを切る

Ajvはスキーマを検証関数に変換してから使う方式なので、コンパイルを起動時に1回だけ行い、生成した関数を使い回す設計にします。リクエストのたびにcompileを呼ぶ実装は、この方式の利点を自分で消してしまいます。

設定で押さえておく値は、次に挙げる4つです。strictモードは既定で有効で、未知のキーワードやformatがあるとcompileが例外を投げる動作になります。allErrorsは既定で無効で、外部から来るデータの検証では無効のままにする運用です。formatを使うならajv-formatsを追加します。利用者向けの文言はajv-errorsのerrorMessageでスキーマ側に寄せ、エラーオブジェクトはそのまま返さずに整形します。

選び分けは「スキーマをJSONのまま他の言語やOpenAPIと共有するか」で決まります。共有するならAjv、TypeScriptの中で閉じるならZodの方が書く量は少なく済みます。

Ajvとは何か:JSON Schemaを検証関数へ変換するJavaScriptライブラリ

Ajvの正式名称とv8.20.0で検証できるスキーマ仕様の対応範囲

AjvはGitHubのajv-validator/ajvで開発されているMITライセンスのOSSで、2026年11月1日時点のスター数は14,866です。npmの週間ダウンロード数は2026年10月23日から29日の集計で約5億1,509万回でした。ESLintやwebpackなど多くのツールが依存しているため、自分で入れた覚えがなくてもnode_modulesに入っていることがよくあります。

扱える仕様は、JSON Schemaのdraft-06、draft-07、2019-09、2020-12と、RFC 8927のJSON Type Definition(JTD)です。公式のJSON Schemaページによると既定のクラスはdraft-07向けで、2019-09と2020-12には、それぞれ別のクラスの読み込みが必要です。draft-04はv8.5.0以降で追加パッケージのajv-draft-04を使います。JSON Schemaそのものの書き方はJSON Schemaとは?構造を検証する書き方とDraft 2020-12の実装で詳しく扱っています。

コンパイル方式の仕組みとZod・Valibotとの設計上の違い

Ajvはスキーマを読み、その検査だけを行うJavaScriptのコードを生成して関数にします。毎回スキーマを解釈し直す方式より検証が速く、公式のGetting startedも、コンパイルした関数を使い回す前提で書かれています。

ZodやValibotはTypeScriptのコードでスキーマを組み立て、型推論によって入力の型も得られる仕組みです。Ajvのスキーマはただのデータ(JSON)なので、ファイルに置いてPythonやGoの検証ライブラリ、OpenAPIの定義と同じものを読ませられます。この違いが選び分けの軸であり、判断の章で詳しく扱う内容です。TypeScript側の書き味はZodの使い方入門と比べると差がつかみやすくなります。

npm install ajvからcompileとvalidateを動かす初回の検証手順

npm install ajvとajv-formatsによるformat検証の有効化

本体とformat用のプラグインを入れます。Ajvは型定義を同梱しているので、TypeScriptでも@types/ajvは要りません。

# 2026-11-01時点: ajv 8.20.0 / ajv-formats 3.0.1
npm install ajv ajv-formats

公式のFormat validationのとおり、v7以降はemailやdate-timeなどのformatが本体から外れ、ajv-formatsに移りました。プラグインを入れずにformat: "email"を書くと、strictモードの既定ではcompileの時点でunknown format "email" ignored in schema at path …という例外になります。v6時代の記事のコードがそのまま動かない原因の多くはここです。

compileで関数を作りvalidate.errorsのinstancePathを読む

Ajvのインスタンスはアプリ全体で1つにし、スキーマごとにcompileした関数を保持します。CommonJSでは次のように書けます。

const Ajv = require("ajv");
const addFormats = require("ajv-formats");

const ajv = new Ajv(); // アプリ全体で1つだけ作る
addFormats(ajv);

const userSchema = {
  type: "object",
  properties: {
    name: { type: "string", minLength: 1, maxLength: 50 },
    email: { type: "string", format: "email", maxLength: 254 },
    age: { type: "integer", minimum: 0 }
  },
  required: ["name", "email"],
  additionalProperties: false
};

// 起動時に1回だけコンパイルし、関数を使い回す
const validateUser = ajv.compile(userSchema);

const input = { name: "山田", age: 20 };
if (!validateUser(input)) {
  console.log(validateUser.errors);
}

この入力はemailが無いので、validateUser.errorsには次の形のオブジェクトが1件入ります。項目の並びはAjvのソースでinstancePath、schemaPath、keyword、params、messageの順です。

[
  {
    instancePath: "",
    schemaPath: "#/required",
    keyword: "required",
    params: { missingProperty: "email" },
    message: "must have required property 'email'"
  }
]

instancePathはJSON Pointer形式で、/ageならageの値が違反したことを示します。v6のdataPathはv8でinstancePathに改名されたため、古い記事のコードを写すと値がundefinedになります。もう1つの注意は、errorsが関数に1つだけの置き場所で、次の呼び出しで上書きされる点です。非同期処理を挟む前に中身を取り出しておきます。

TypeScriptで書くときのJSONSchemaTypeと型ガードの使い方

公式のUsing with TypeScriptによると、compileで作った関数は型ガードとして働き、検証に通ったあとの値は指定した型に絞られます。strictNullChecksが有効であることが前提です。

import Ajv, { JSONSchemaType } from "ajv";

interface User {
  name: string;
  age?: number;
}

const schema: JSONSchemaType<User> = {
  type: "object",
  properties: {
    name: { type: "string", minLength: 1 },
    age: { type: "integer", nullable: true }
  },
  required: ["name"],
  additionalProperties: false
};

const ajv = new Ajv();
const validate = ajv.compile(schema);

export function parseUser(body: unknown): User {
  if (validate(body)) return body; // ここで body は User 型に絞られる
  throw new Error(ajv.errorsText(validate.errors));
}

JSONSchemaTypeはインターフェースとスキーマの食い違いをコンパイル時に指摘してくれます。省略可能なプロパティにはnullable: trueを書く必要があり、ここで型エラーに戸惑う人が多い部分です。unionの網羅までは確かめられないと公式に書かれているので、anyOfを多用するスキーマでは型を手書きで併記します。

strictモードとallErrorsと型変換の既定値を確かめるAjvの設定

strictモードが未知のキーワードとformatでcompileを止める動作

公式のStrict modeは、v7から既定で有効になった仕組みです。minLengthをminLenghtと打ち間違えると、Ajvはstrict mode: unknown keyword: "minLenght"という例外をcompile時に投げます。JSON Schemaの仕様上は未知のキーワードを無視してよいため、緩い検証器では誤字に気づけないまま、チェックが効かない状態で本番に出ます。

この既定は外さずに使います。社内独自のキーワード(例:x-label)をスキーマに書きたいときは、ajv.addKeyword("x-label")で登録すれば例外になりません。strict: falseで全体を止めるのは、誤字検知まで捨てることになるので避けます。

allErrorsの既定は最初の1件で止まる理由と本番での扱い

既定のallErrors: falseでは、最初に見つかった違反で検証を打ち切ります。フォームの全項目にまとめてエラーを出したいのでtrueにしたくなりますが、公式のSecurity considerationsは見出しで「本番でallErrorsを使わない」と書いています。全件を調べると、巨大な配列や重い正規表現を含む入力で処理時間が伸び、DoSの入口になるためです。

Fastifyも公式のValidation and SerializationでallErrorsを明示的にfalseにしており、trueではDoS攻撃が可能だと注記しています。全件表示が要るのは管理画面の設定ファイル検証など、入力元が信頼できる場面に限ります。公開APIは1件ずつ返し、画面側の即時チェックで補うのが現実的な分担です。

coerceTypes・useDefaults・removeAdditionalの作用

次の3つは、検証のついでに渡したオブジェクトそのものを書き換えます。既定はすべて無効です(公式のAjv options)。

オプション 既定値 指定できる値 入力への作用
coerceTypes false true / “array” 文字列”3″を数値3に変えるなど型を合わせる
useDefaults false true / “empty” 欠けた項目にスキーマのdefault値を入れる
removeAdditional false true / “all” / “failing” 定義外のプロパティを削除する
allErrors false true 書き換えはせず全違反を集める

クエリ文字列はすべて文字列で届くため、?page=3をintegerで検証するならcoerceTypesが要ります。Fastifyは既定でcoerceTypes: "array"、useDefaults: true、removeAdditional: trueを有効にする設定です。removeAdditional: "all"はadditionalPropertiesの指定に関係なく余分な項目を消すので、監査のために元の入力を残したい処理では、検証前に複製を取っておきます。

Draft 2020-12を使うときのajv/dist/2020への切り替え条件

prefixItemsや$dynamicRefなどJSON Schema 2020-12の語を使うなら、読み込むクラスを変えます。既定のクラスに2020-12の$schemaを書いたスキーマを渡すと、メタスキーマが見つからずにcompileが失敗します。

// draft-07用の既定クラスではなく、2020-12用のクラスを読み込む
const Ajv2020 = require("ajv/dist/2020");
const ajv = new Ajv2020();

const validatePair = ajv.compile({
  $schema: "https://json-schema.org/draft/2020-12/schema",
  type: "array",
  prefixItems: [{ type: "string" }, { type: "number" }],
  items: false // 3つ目以降の要素を許さない
});

2020-12と以前の版は同じインスタンスに混在できません。OpenAPI 3.1の定義から取り込むスキーマは2020-12系なのでAjv2020、手書きの既存スキーマがdraft-07ならインスタンスを2つに分けます。新しい語が要らないなら、公式が生成コードの効率を理由に勧めるdraft-07のままで構いません。

ajv-errorsのerrorMessageによる日本語エラーとAPIの400応答整形

ajv-errorsのerrorMessageで項目ごとの日本語メッセージを書く

Ajvのメッセージはmust NOT have fewer than 1 charactersのような英文です。画面に出す文言は、公式のajv-errorsのerrorMessageキーワードでスキーマ側に書きます。ajv-errorsのv3がAjv v8に対応し、allErrors: trueが必須条件です。falseのまま組み込むとajv-errors: Ajv option allErrors must be trueで起動時に止まります。

const Ajv = require("ajv");
const ajvErrors = require("ajv-errors");

// ajv-errors は allErrors: true が前提。入力元を信頼できる用途に限る
const ajv = new Ajv({ allErrors: true });
ajvErrors(ajv);

const validateSignup = ajv.compile({
  type: "object",
  properties: {
    name: { type: "string", minLength: 1, maxLength: 50 },
    age: { type: "integer", minimum: 18 }
  },
  required: ["name"],
  additionalProperties: false,
  errorMessage: {
    required: { name: "氏名は必須です" },
    properties: {
      name: "氏名は1〜50文字で入力してください",
      age: "年齢は18以上の整数で入力してください"
    },
    additionalProperties: "想定していない項目が含まれています"
  }
});

前の章のとおり、allErrorsを有効にする以上、このインスタンスは管理画面や社内ツールの入力に使い、公開APIの入口とは分けます。公開APIで日本語を返したいときは、keywordとinstancePathを見て自前の対応表から文言を引くほうが安全です。

Expressのミドルウェアで400とProblem Details形式を返す実装

エラーオブジェクトをそのまま応答に載せると、schemaPathでスキーマの構造が外から読めます。verbose: trueでは、スキーマ本体と入力値まで応答に載る仕様です。応答に出すのは項目の位置と理由だけに絞ります。

// compile済みの関数を受け取り、Expressのミドルウェアを返す
function validateBody(validate) {
  return (req, res, next) => {
    if (validate(req.body)) return next();
    const errors = (validate.errors || []).map((e) => ({
      pointer: e.instancePath || "/",
      reason: e.keyword // required, minLength などの機械向けの理由
    }));
    res.status(400).type("application/problem+json").json({
      type: "about:blank",
      title: "入力内容に誤りがあります",
      status: 400,
      errors
    });
  };
}

app.post("/users", express.json({ limit: "100kb" }), validateBody(validateUser), createUser);

express.jsonのlimitで本文の大きさを先に抑え、Ajvに渡る入力の上限を決めておきます。エラーコードの付け方やクライアントの再試行判断まで含めた応答設計は、APIエラーレスポンス設計とは?コード体系・リトライ判断・漏洩防止の実装手順にまとめています。

コンパイルを起動時に限定するスキーマ管理とstandaloneとReDoS対策

単一インスタンスとaddSchemaとgetSchemaで再コンパイルを防ぐ

公式のManaging schemasは、各スキーマを1回だけコンパイルすることと、アプリ全体で単一のインスタンスを使うことを勧めています。Ajvはスキーマのオブジェクトをキーにキャッシュしますが、リクエストごとにスキーマを組み立て直すと別オブジェクトになり、毎回コンパイルが走ります。

// schemas.js: 起動時に$idで登録し、使う側はIDで引く
const ajv = new Ajv();
addFormats(ajv);
ajv.addSchema(userSchema, "user");
ajv.addSchema(orderSchema, "order");

// ハンドラ側: 登録済みのコンパイル結果を取り出すだけ
const validate = ajv.getSchema("user");

同じIDでaddSchemaを2回呼ぶとschema with key or id "user" already existsで例外になるため、登録はモジュールの読み込み時に1回だけ行います。getSchemaは初回の呼び出しでコンパイルし、以後は同じ関数を返します。

standaloneコードの生成とCSPで動的評価が禁止される環境への対応

Ajvは生成したコードを実行時にnew Functionで評価します。ブラウザのContent Security Policyでunsafe-evalを許していない画面や、コールドスタートを短くしたいサーバーレス関数では、公式のStandalone validation codeの機能でビルド時に検証関数をファイルへ書き出します。

// build-validators.js: ビルド時に実行し、生成したファイルを配布物に含める
const fs = require("fs");
const Ajv = require("ajv");
const standaloneCode = require("ajv/dist/standalone").default;

const ajv = new Ajv({ code: { source: true, esm: true } });
const validate = ajv.compile({
  type: "object",
  properties: { id: { type: "integer", minimum: 1 } },
  required: ["id"]
});
fs.writeFileSync("validate-id.mjs", standaloneCode(ajv, validate));

esm: trueを外すとCommonJSで出力されます。生成物は実行時にAjv本体を読み込まないので、ブラウザ向けのバンドルからAjvを外せる仕組みです。スキーマを変えたら生成し直す手順が増えるため、CIのビルドに組み込んで手作業を残さないようにします。

ReDoSと巨大入力に備えるmaxLengthとpatternとRE2の指定

公式のSecurity considerationsは、既定の正規表現エンジン(Node.js組み込み)の最悪計算量が指数時間であり、短い文字列でも処理が止まりうると警告しています。対策は優先度の高い順に次のとおりです。

  1. patternやformatを付けた文字列には必ずmaxLengthを書く
  2. uniqueItemsを付けた配列にはmaxItemsを書く
  3. 外部入力に自前のpatternを使うなら、new Ajv({ code: { regExp: RE2 } })で線形時間のnode-re2に差し替える
  4. 利用者がアップロードしたスキーマはサイズを制限し、原則として受け付けない

4つ目は見落とされがちです。Ajvはスキーマを自社のコードと同じく信頼できるものとして扱う前提のライブラリです。SaaSの利用者に独自スキーマを登録させる機能を作るなら、検証用のプロセスを分けるなど、Ajvの外側で隔離を用意します。入力検証全体の守備範囲は入力バリデーションとは?防げる攻撃の範囲と許可リスト設計で整理しています。

Ajvを採用する条件と見送る場面:スキーマ共有の有無で決める判断

Ajvを選ぶ場面はOpenAPIや他言語とスキーマを共有するAPI開発

Ajvを選ぶのは、検証ルールをJSONのまま複数の場所で使う場合です。OpenAPIで定義したリクエスト本文をサーバーの入口でそのまま検証する、PythonのバッチとNode.jsのAPIで同じスキーマを読む、Fastifyで入力検証と応答の直列化を1つのスキーマで済ませる、といった構成ではAjvが素直に収まります。OpenAPIの側の設計はOpenAPIとは?Swaggerとの違いとYAMLの書き方が参考になります。

外部システムとの連携で、相手が配るJSON Schemaに合わせて受信データを検査する場面でもAjvが第一候補です。連携先ごとのスキーマ管理やエラー時の再送設計まで含めて実装を任せたい場合は、一創のAPI開発・システム連携で要件の整理から対応しています。

Ajvを見送る場面はTypeScriptの中で閉じるフォームの入力検証

スキーマを他へ渡す予定がなく、ReactのフォームやNext.jsのServer Actionsのように検証がTypeScriptの中で完結するなら、Ajvは採りません。JSONSchemaTypeとインターフェースを二重に書く手間が残り、nullableやunionで型エラーと格闘する時間が増えるからです。この用途ではZod(2026年11月1日時点で4.6.7)か、バンドルを小さくしたいならValibotを選びます。

迷ったときの線引きは1つです。スキーマを「データ」として保存・配布するならAjv、「コード」として型と一緒に書くならZod系です。両方が必要なAPIでは、境界の入口だけAjvで検証し、内部の処理はZodで型を付けるという併用も成り立ちます。

よくある質問

Ajvを導入するときに寄せられやすい質問をまとめました。

Ajvの読み方と正式名称は何ですか?

正式名称はAnother JSON Schema Validatorで、AjvはそのAとJとVを取った略称です。公式に定められた読み方は見当たらず、日本語の現場では「エージェイブイ」とアルファベットで読む人が多くいます。npmのパッケージ名は小文字のajvです。ドキュメントはajv.js.orgにまとまっています。2020年にはOpenJS Foundationのプロジェクトに加わっています。

Ajvのエラーメッセージを日本語にする方法はありますか?

方法は2つです。1つはajv-errorsを入れ、スキーマにerrorMessageで日本語の文言を書く方法で、allErrors: trueが必須になります。もう1つはajv-i18n(4.2.0)で既定メッセージを翻訳する方法です。公開APIではallErrorsを有効にしない方が安全なため、keywordとinstancePathから自前の対応表で文言を引く方法も現実的な選択肢になります。

Ajvでformat: “email”が効かないのはなぜですか?

v7以降、emailやdate-timeなどのformatは本体から外れ、ajv-formatsに移ったためです。npm install ajv-formatsのあとaddFormats(ajv)を呼ぶと有効になります。strictモードが既定で有効なので、プラグインが無いままだとcompileの時点でunknown formatの例外が出ます。v6向けの記事のコードを写したときによく起きる症状です。

AjvとZodはどちらを使えばよいですか?

スキーマをJSONとして他の言語やOpenAPIと共有するならAjv、TypeScriptの中で検証と型を一緒に書くならZodです。Ajvはスキーマを関数にコンパイルして使い回すので、同じスキーマを大量のリクエストに当てるサーバーの入口に向きます。Zodは型推論で入力の型が自動で得られ、フォームや社内APIの実装量を減らせます。

Ajvのcompileは毎回呼んでも問題ありませんか?

呼ぶたびにコード生成が走るので、毎回の呼び出しは避ける設計です。Ajvはスキーマのオブジェクトをキーにキャッシュしますが、リクエストごとにスキーマを組み立て直すと別オブジェクトと見なされ、毎回のコンパイルが必要になる仕組みです。起動時にcompileかaddSchemaで登録し、ハンドラでは生成済みの関数かgetSchemaの結果を使います。インスタンスもアプリ全体で1つに保ちます。

関連記事

お気に入りに入れた記事の一覧

一創の公式 SNS

X では毎朝の新着記事と IT ニュースの速報を、YouTube・TikTok・Instagram・LinkedIn・Facebook では記事の解説動画を配信しています。

この記事は以下の記事からリンクされています

資料請求

今日のトレンド記事 直近 24 時間で、いつもより多く読まれている記事

  1. 2026.10.29 テックブログ ソニー生命の不正アクセス:38.5万件の対象確認と窓口、停止中の手続き
  2. 2026.10.31 テックブログ 松屋の不正アクセス:237万件の対象確認と窓口、漏れたパスワードの守り方
  3. 2026.10.31 テックブログ 無印良品の情報漏洩:MUJIアプリ最大2,726万件の対象確認となりすましメール対策
  4. 2026.10.27 テックブログ Joshin(ジョーシン)不正アクセス:最大600万件の対象とサブスク初期パスワードの対処
  5. 2026.10.28 テックブログ リンツ不正アクセス:314万件の対象確認とパスワード強制リセット後の対処

RELATED POSTS 関連記事

目次