joi の使い方 — JavaScriptで最も強力なスキーマバリデーションライブラリ
一言でいうと
joi は、JavaScriptオブジェクトのスキーマ定義とバリデーションを行うライブラリです。直感的なチェーンAPIで複雑なバリデーションルールを宣言的に記述でき、APIリクエストのボディ検証やフォーム入力チェック、設定ファイルの検証などに広く使われています。
どんな時に使う?
- APIリクエストのバリデーション — Express/Fastifyなどで受け取るリクエストボディ・クエリパラメータの型と値を厳密に検証したい時
- 環境変数・設定ファイルの検証 — アプリ起動時に必要な設定値が正しい形式で揃っているか確認したい時
- ユーザー入力のサーバーサイドバリデーション — フォームから送信されたデータが期待するスキーマに合致するか検証したい時
インストール
# npm
npm install joi
# yarn
yarn add joi
# pnpm
pnpm add joi
※ 本記事は joi v18.x 系を対象としています。v16以前とはAPIに差異がある場合があります。
joi の基本的な使い方
import Joi from 'joi';
// スキーマを定義
const userSchema = Joi.object({
username: Joi.string().alphanum().min(3).max(30).required(),
email: Joi.string().email().required(),
age: Joi.number().integer().min(0).max(150),
role: Joi.string().valid('admin', 'editor', 'viewer').default('viewer'),
});
// バリデーション実行
const input = {
username: 'taro123',
email: 'taro@example.com',
age: 28,
};
const { error, value } = userSchema.validate(input);
if (error) {
console.error('バリデーションエラー:', error.details);
} else {
console.log('検証済みデータ:', value);
// => { username: 'taro123', email: 'taro@example.com', age: 28, role: 'viewer' }
}
ポイントは validate() の戻り値です。error が undefined なら検証成功、value にはデフォルト値の適用や型変換が反映された「クリーンなデータ」が入ります。
よく使うAPI
1. Joi.string() — 文字列バリデーション
const schema = Joi.object({
// メールアドレス形式
email: Joi.string().email().required(),
// 正規表現によるパターンマッチ
zipCode: Joi.string().pattern(/^\d{3}-\d{4}$/).message('郵便番号はXXX-XXXXの形式で入力してください'),
// URI形式
website: Joi.string().uri({ scheme: ['http', 'https'] }),
// 長さ制限 + トリム
bio: Joi.string().trim().max(500),
});
2. Joi.number() — 数値バリデーション
const schema = Joi.object({
// 整数のみ、範囲指定
age: Joi.number().integer().min(0).max(150),
// 正の数
price: Joi.number().positive().precision(2),
// ポート番号
port: Joi.number().port(), // 0〜65535
});
3. Joi.array() / Joi.object() — ネスト構造のバリデーション
const orderSchema = Joi.object({
orderId: Joi.string().uuid().required(),
items: Joi.array()
.items(
Joi.object({
productId: Joi.string().required(),
quantity: Joi.number().integer().min(1).required(),
note: Joi.string().allow('').optional(),
})
)
.min(1)
.required(),
metadata: Joi.object().pattern(
Joi.string(), // キーは文字列
Joi.string() // 値も文字列
),
});
4. Joi.alternatives() / .when() — 条件分岐バリデーション
const paymentSchema = Joi.object({
method: Joi.string().valid('credit_card', 'bank_transfer').required(),
// method の値に応じてバリデーションを切り替え
cardNumber: Joi.when('method', {
is: 'credit_card',
then: Joi.string().creditCard().required(),
otherwise: Joi.forbidden(),
}),
bankAccount: Joi.when('method', {
is: 'bank_transfer',
then: Joi.string().required(),
otherwise: Joi.forbidden(),
}),
});
// alternatives を使った型の分岐
const idSchema = Joi.alternatives().try(
Joi.string().uuid(),
Joi.number().integer().positive()
);
5. Joi.any().custom() — カスタムバリデーション
const schema = Joi.object({
password: Joi.string().min(8).required(),
confirmPassword: Joi.string()
.valid(Joi.ref('password'))
.required()
.messages({ 'any.only': 'パスワードが一致しません' }),
startDate: Joi.date().iso().required(),
endDate: Joi.date()
.iso()
.greater(Joi.ref('startDate'))
.required()
.messages({ 'date.greater': '終了日は開始日より後にしてください' }),
// 完全にカスタムなロジック
evenNumber: Joi.number().custom((value, helpers) => {
if (value % 2 !== 0) {
return helpers.error('any.invalid');
}
return value;
}, '偶数チェック'),
});
バリデーションオプション
validate() の第2引数でバリデーションの挙動を制御できます。
const { error, value } = schema.validate(input, {
abortEarly: false, // false にすると全エラーを収集(デフォルト: true)
stripUnknown: true, // スキーマに定義されていないキーを除去
allowUnknown: true, // 未定義キーをエラーにしない
convert: true, // 文字列 "123" → 数値 123 のような型変換(デフォルト: true)
});
特に abortEarly: false はフォームバリデーションで全フィールドのエラーを一度に返したい場合に必須です。
類似パッケージとの比較
| 特徴 | joi | zod | yup | ajv |
|---|---|---|---|---|
| TypeScript推論 | △(別途型定義が必要) | ◎(ネイティブ対応) | ○ | △ |
| バンドルサイズ | 大きめ(約150KB) | 小さめ(約13KB) | 中程度(約40KB) | 小さめ(約30KB) |
| スキーマ記法 | チェーンAPI | チェーンAPI | チェーンAPI | JSON Schema |
| ブラウザ対応 | △(v16以降非推奨) | ◎ | ◎ | ◎ |
| エコシステム | hapi連携が強力 | tRPC/React Hook Form | Formik連携 | OpenAPI連携 |
| 条件分岐 | ◎(.when()が強力) | ○ | ○ | ○ |
| カスタムエラーメッセージ | ◎ | ○ | ◎ | △ |
選定の目安:
- サーバーサイド(Node.js)中心 → joi が最も表現力が高い
- TypeScriptファースト / フロント・バック共用 → zod が現在の主流
- Formikと組み合わせたい → yup
- JSON Schemaベースで運用したい → ajv
注意点・Tips
1. joi はサーバーサイド向け
joi v16以降、ブラウザ向けのビルドは公式にサポートされていません。フロントエンドで使いたい場合は zod や yup を検討してください。
2. TypeScriptの型推論を活用する
joi 単体ではスキーマから TypeScript の型を自動推論できません。手動で型を定義するか、ヘルパーを使います。
import Joi from 'joi';
const userSchema = Joi.object({
username: Joi.string().required(),
email: Joi.string().email().required(),
age: Joi.number().integer(),
});
// スキーマから型を抽出(joi v17+)
type User = Joi.extractType<typeof userSchema>;
// ただし精度に限界があるため、手動定義の方が確実
// 実用的なパターン: 型を先に定義してスキーマと合わせる
interface User {
username: string;
email: string;
age?: number;
}
const { error, value } = userSchema.validate(input) as {
error: Joi.ValidationError | undefined;
value: User;
};
3. エラーメッセージの日本語化
const schema = Joi.object({
name: Joi.string().min(1).max(50).required().messages({
'string.empty': '名前を入力してください',
'string.min': '名前は{#limit}文字以上で入力してください',
'string.max': '名前は{#limit}文字以下で入力してください',
'any.required': '名前は必須です',
}),
});
4. Express ミドルウェアとして使う
import { Request, Response, NextFunction } from 'express';
import Joi from 'joi';
function validate(schema: Joi.ObjectSchema) {
return (req: Request, res: Response, next: NextFunction) => {
const { error, value } = schema.validate(req.body, {
abortEarly: false,
stripUnknown: true,
});
if (error) {
const messages = error.details.map((d) => d.message);
return res.status(400).json({ errors: messages });
}
req.body = value; // クリーンなデータで上書き
next();
};
}
// 使用例
app.post('/users', validate(userSchema), (req, res) => {
// req.body は検証済み
});
5. assert() で例外を投げる
設定値の検証など「不正なら即停止」したい場面では assert() が便利です。
const envSchema = Joi.object({
NODE_ENV: Joi.string().valid('development', 'production', 'test').required(),
PORT: Joi.number().port().default(3000),
DATABASE_URL: Joi.string().uri().required(),
}).unknown(true); // process.env には他のキーも含まれるため
// 不正なら ValidationError をスロー
const config = Joi.attempt(process.env, envSchema);
console.log(`Server starting on port ${config.PORT}`);
まとめ
joi は、Node.js サーバーサイドにおけるデータバリデーションの定番ライブラリです。チェーンAPIによる宣言的なスキーマ定義、条件分岐(.when())、カスタムエラーメッセージなど、複雑なビジネスルールにも対応できる表現力が最大の強みです。
一方で、TypeScript の型推論やブラウザ対応を重視する場合は zod への移行も選択肢に入ります。既存プロジェクトで joi を使っているなら無理に移行する必要はありませんが、新規プロジェクトでは用途に応じて比較検討するとよいでしょう。