joi の使い方

Object schema validation

v18.1.217.4M/週BSD-3-Clauseバリデーション
AI生成コンテンツ

この記事はAIによって生成されました。内容の正確性は保証されません。最新の情報は公式ドキュメントをご確認ください。

joi の使い方 — JavaScriptで最も強力なスキーマバリデーションライブラリ

一言でいうと

joi は、JavaScriptオブジェクトのスキーマ定義とバリデーションを行うライブラリです。直感的なチェーンAPIで複雑なバリデーションルールを宣言的に記述でき、APIリクエストのボディ検証やフォーム入力チェック、設定ファイルの検証などに広く使われています。


どんな時に使う?

  1. APIリクエストのバリデーション — Express/Fastifyなどで受け取るリクエストボディ・クエリパラメータの型と値を厳密に検証したい時
  2. 環境変数・設定ファイルの検証 — アプリ起動時に必要な設定値が正しい形式で揃っているか確認したい時
  3. ユーザー入力のサーバーサイドバリデーション — フォームから送信されたデータが期待するスキーマに合致するか検証したい時

インストール

# 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() の戻り値です。errorundefined なら検証成功、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 はフォームバリデーションで全フィールドのエラーを一度に返したい場合に必須です。


類似パッケージとの比較

特徴joizodyupajv
TypeScript推論△(別途型定義が必要)◎(ネイティブ対応)
バンドルサイズ大きめ(約150KB)小さめ(約13KB)中程度(約40KB)小さめ(約30KB)
スキーマ記法チェーンAPIチェーンAPIチェーンAPIJSON Schema
ブラウザ対応△(v16以降非推奨)
エコシステムhapi連携が強力tRPC/React Hook FormFormik連携OpenAPI連携
条件分岐◎(.when()が強力)
カスタムエラーメッセージ

選定の目安:

  • サーバーサイド(Node.js)中心 → joi が最も表現力が高い
  • TypeScriptファースト / フロント・バック共用 → zod が現在の主流
  • Formikと組み合わせたい → yup
  • JSON Schemaベースで運用したい → ajv

注意点・Tips

1. joi はサーバーサイド向け

joi v16以降、ブラウザ向けのビルドは公式にサポートされていません。フロントエンドで使いたい場合は zodyup を検討してください。

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 を使っているなら無理に移行する必要はありませんが、新規プロジェクトでは用途に応じて比較検討するとよいでしょう。