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組み込み)の最悪計算量が指数時間であり、短い文字列でも処理が止まりうると警告しています。対策は優先度の高い順に次のとおりです。
patternやformatを付けた文字列には必ずmaxLengthを書くuniqueItemsを付けた配列にはmaxItemsを書く- 外部入力に自前の
patternを使うなら、new Ajv({ code: { regExp: RE2 } })で線形時間のnode-re2に差し替える - 利用者がアップロードしたスキーマはサイズを制限し、原則として受け付けない
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つに保ちます。
関連記事
- JSON Schemaとは?JSONの構造を検証する書き方とDraft 2020-12の実装を実装者目線で解説【2026年版】:Ajvに渡すスキーマの文法と版の違いを確認できます
- バリデーションとは?入力検証の目的・種類・実装パターンと設計を実装者視点で解説:Ajvを置く層とほかの検証との役割分担が分かります
- APIエラーレスポンス設計とは?コード体系・リトライ判断・漏洩防止の実装手順:Ajvの検証結果を返す応答の形を詰められます
- Zodの使い方入門|npm install zodとz.infer・safeParse:TypeScriptで閉じる場合の選択肢として比べられます
- OpenAPIとは?Swaggerとの違い(仕様とツール)とYAMLの書き方・3.0〜3.2の差分:Ajvで検証するスキーマの出どころになる仕様です