Joi
GitHub概要
スター21,122
ウォッチ176
フォーク1,518
作成日:2012年9月16日
言語:JavaScript
ライセンス:Other
トピックス
hapijavascriptschemavalidation
スター履歴
データ取得日時: 2025/7/18 07:04
ライブラリ
Joi
概要
Joiは「The most powerful data validation library for JS」として開発されたNode.js向けの強力で成熟したスキーマバリデーションライブラリです。豊富なバリデーションルールと柔軟なカスタマイズ機能により、複雑なバリデーション要件にも対応可能。長年の実績と豊富な機能でサーバーサイド開発において信頼される存在として、特に複雑なAPIバリデーションで選ばれることが多い老舗ライブラリです。Express.jsとの統合により堅牢なWebアプリケーション開発を支援します。
詳細
Joi 17.13.3は2025年現在の最新版で、13,109以上のnpmプロジェクトで使用されているNode.jsエコシステムの標準的なバリデーションライブラリです。スキーマ記述言語として包括的かつ強力な機能を提供し、文字列、数値、ブール値、配列、オブジェクト、日付など豊富な型のバリデーションが可能。Express.jsのミドルウェアとしての統合により、リクエストレベルでのデータ検証パイプラインを構築し、フロントエンドからの信頼できないデータを安全に処理します。
主な特徴
- 包括的なバリデーション: 文字列から複雑なオブジェクトまで幅広い型対応
- 豊富なルールセット: 詳細な制約条件とカスタムバリデーション機能
- Express.js統合: ミドルウェアとしての簡単な統合とエラーハンドリング
- 柔軟なスキーマ定義: ネストしたオブジェクトと条件付きバリデーション
- 詳細なエラー報告: 具体的で分かりやすいバリデーションエラーメッセージ
- 高いカスタマイズ性: プラグイン機能とカスタムバリデーター対応
メリット・デメリット
メリット
- Node.jsエコシステムでの長年の実績と安定性
- 複雑なAPIバリデーション要件に対応する豊富な機能
- Express.jsとの優れた統合とミドルウェア対応
- 包括的な公式ドキュメントと充実したコミュニティサポート
- 企業レベルのサーバーサイドアプリケーションでの採用実績
- 詳細で分かりやすいエラーメッセージとデバッグ支援
デメリット
- ブラウザ(クライアントサイド)での使用に制限
- バンドルサイズが大きめ(軽量なバリデーションには向かない)
- TypeScriptでの型推論サポートが限定的
- モダンなJavaScriptフレームワークとの統合で追加設定が必要
- 関数型プログラミングスタイルに不向き
- パフォーマンス重視のアプリケーションではオーバーヘッド
参考ページ
書き方の例
インストールと基本セットアップ
# Joiのインストール
npm install joi
# TypeScript用の型定義(コミュニティ製)
npm install @types/joi
# Yarnを使用する場合
yarn add joi
yarn add -D @types/joi
基本的なスキーマ定義とバリデーション
const Joi = require('joi');
// 基本的なスキーマ定義
const userSchema = Joi.object({
username: Joi.string()
.alphanum()
.min(3)
.max(30)
.required(),
email: Joi.string()
.email({ minDomainSegments: 2, tlds: { allow: ['com', 'net'] } })
.required(),
password: Joi.string()
.pattern(new RegExp('^[a-zA-Z0-9]{3,30}$'))
.required(),
age: Joi.number()
.integer()
.min(18)
.max(100),
birth_year: Joi.number()
.integer()
.min(1900)
.max(2013),
access_token: [
Joi.string(),
Joi.number()
]
});
// データのバリデーション
const userData = {
username: 'john123',
email: '[email protected]',
password: 'mypassword123',
age: 25
};
// 同期バリデーション
const { error, value } = userSchema.validate(userData);
if (error) {
console.log('バリデーションエラー:', error.details);
} else {
console.log('バリデーション成功:', value);
}
// 非同期バリデーション
async function validateUserAsync(data) {
try {
const value = await userSchema.validateAsync(data);
console.log('バリデーション成功:', value);
return value;
} catch (err) {
console.log('バリデーションエラー:', err.details);
throw err;
}
}
// プリミティブ型のバリデーション例
const stringSchema = Joi.string().min(2).max(10);
const numberSchema = Joi.number().positive();
const booleanSchema = Joi.boolean();
const dateSchema = Joi.date().iso();
console.log(stringSchema.validate('hello')); // 成功
console.log(numberSchema.validate(42)); // 成功
console.log(booleanSchema.validate(true)); // 成功
高度なバリデーションルールとカスタムバリデーター
// 複雑なオブジェクトスキーマ
const productSchema = Joi.object({
name: Joi.string()
.min(1)
.max(100)
.required()
.description('商品名'),
price: Joi.number()
.positive()
.precision(2)
.required()
.description('価格(小数点2桁まで)'),
category: Joi.string()
.valid('electronics', 'clothing', 'books', 'food')
.required(),
tags: Joi.array()
.items(Joi.string().min(1))
.min(1)
.max(10)
.unique(),
specifications: Joi.object({
weight: Joi.number().positive().unit('kg'),
dimensions: Joi.object({
width: Joi.number().positive(),
height: Joi.number().positive(),
depth: Joi.number().positive()
}),
color: Joi.string().hex(),
material: Joi.string().optional()
}).optional(),
inStock: Joi.boolean().default(true),
metadata: Joi.object().pattern(
Joi.string(),
[Joi.string(), Joi.number(), Joi.boolean()]
).optional()
});
// 条件付きバリデーション
const conditionalSchema = Joi.object({
type: Joi.string().valid('user', 'admin').required(),
username: Joi.string().when('type', {
is: 'admin',
then: Joi.string().min(5).required(),
otherwise: Joi.string().min(3).required()
}),
permissions: Joi.array().when('type', {
is: 'admin',
then: Joi.array().items(Joi.string()).min(1).required(),
otherwise: Joi.forbidden()
})
});
// カスタムバリデーター
const customSchema = Joi.object({
email: Joi.string().custom((value, helpers) => {
// カスタムメールバリデーション
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(value)) {
return helpers.error('any.invalid');
}
// 禁止ドメインのチェック
const forbiddenDomains = ['spam.com', 'temp.mail'];
const domain = value.split('@')[1];
if (forbiddenDomains.includes(domain)) {
return helpers.message('このドメインは使用できません');
}
return value.toLowerCase();
}, 'カスタムメールバリデーション'),
phoneNumber: Joi.string().custom((value, helpers) => {
// 日本の電話番号形式をチェック
const phoneRegex = /^(\+81|0)\d{1,4}-\d{1,4}-\d{4}$/;
if (!phoneRegex.test(value)) {
return helpers.error('string.pattern', { value });
}
return value;
}),
password: Joi.string().custom((value, helpers) => {
// 複雑なパスワード要件
if (value.length < 8) {
return helpers.message('パスワードは8文字以上である必要があります');
}
if (!/[A-Z]/.test(value)) {
return helpers.message('パスワードには大文字を含める必要があります');
}
if (!/[a-z]/.test(value)) {
return helpers.message('パスワードには小文字を含める必要があります');
}
if (!/\d/.test(value)) {
return helpers.message('パスワードには数字を含める必要があります');
}
if (!/[!@#$%^&*(),.?":{}|<>]/.test(value)) {
return helpers.message('パスワードには特殊文字を含める必要があります');
}
return value;
})
});
// 配列の詳細バリデーション
const arraySchema = Joi.object({
numbers: Joi.array()
.items(Joi.number().integer().min(1).max(100))
.min(1)
.max(10)
.unique()
.sort(),
users: Joi.array().items(
Joi.object({
name: Joi.string().required(),
role: Joi.string().valid('user', 'admin').default('user')
})
).has(Joi.object({ role: 'admin' })) // 少なくとも1人のadminが必要
});
フレームワーク統合(Express、NestJS、Fastify等)
// Express.jsとの統合例
const express = require('express');
const Joi = require('joi');
const app = express();
app.use(express.json());
// バリデーションミドルウェアの作成
const validate = (schema, property = 'body') => {
return (req, res, next) => {
const { error, value } = schema.validate(req[property], {
abortEarly: false, // 全てのエラーを取得
allowUnknown: true, // 未知のプロパティを許可
stripUnknown: true // 未知のプロパティを削除
});
if (error) {
const errors = error.details.map(detail => ({
field: detail.path.join('.'),
message: detail.message,
value: detail.context.value
}));
return res.status(400).json({
success: false,
message: 'バリデーションエラー',
errors: errors
});
}
// バリデーション済みデータで置き換え
req[property] = value;
next();
};
};
// ユーザー作成API
const createUserSchema = Joi.object({
username: Joi.string().alphanum().min(3).max(30).required(),
email: Joi.string().email().required(),
password: Joi.string().min(8).required(),
profile: Joi.object({
firstName: Joi.string().required(),
lastName: Joi.string().required(),
age: Joi.number().integer().min(18).max(120)
}).optional()
});
app.post('/api/users', validate(createUserSchema), (req, res) => {
// req.bodyは既にバリデーション済み
console.log('検証済みユーザーデータ:', req.body);
// ユーザー作成ロジック
res.json({
success: true,
message: 'ユーザーが正常に作成されました',
data: req.body
});
});
// クエリパラメータのバリデーション
const getUsersQuerySchema = Joi.object({
page: Joi.number().integer().min(1).default(1),
limit: Joi.number().integer().min(1).max(100).default(10),
sort: Joi.string().valid('name', 'email', 'created_at').default('created_at'),
order: Joi.string().valid('asc', 'desc').default('desc'),
search: Joi.string().min(2).optional()
});
app.get('/api/users', validate(getUsersQuerySchema, 'query'), (req, res) => {
console.log('検証済みクエリパラメータ:', req.query);
// ユーザー取得ロジック
res.json({
success: true,
data: [],
pagination: {
page: req.query.page,
limit: req.query.limit,
total: 0
}
});
});
// NestJSとの統合例(TypeScript)
/*
import { Injectable, PipeTransform, BadRequestException } from '@nestjs/common';
import * as Joi from 'joi';
@Injectable()
export class JoiValidationPipe implements PipeTransform {
constructor(private schema: Joi.ObjectSchema) {}
transform(value: any) {
const { error, value: validatedValue } = this.schema.validate(value);
if (error) {
throw new BadRequestException('バリデーションエラー');
}
return validatedValue;
}
}
// 使用例
@Post('users')
@UsePipes(new JoiValidationPipe(createUserSchema))
createUser(@Body() createUserDto: any) {
// バリデーション済みデータの使用
return this.userService.create(createUserDto);
}
*/
// Fastifyとの統合例
/*
const fastify = require('fastify')({ logger: true });
const userSchema = {
body: {
type: 'object',
properties: {
username: { type: 'string', minLength: 3, maxLength: 30 },
email: { type: 'string', format: 'email' },
password: { type: 'string', minLength: 8 }
},
required: ['username', 'email', 'password']
}
};
fastify.post('/users', { schema: userSchema }, async (request, reply) => {
// request.bodyは自動的にバリデーション済み
return { success: true, data: request.body };
});
*/
エラーハンドリングとカスタムエラーメッセージ
// 詳細なエラーハンドリング
const detailedSchema = Joi.object({
name: Joi.string().min(2).max(50).required().messages({
'string.base': '名前は文字列である必要があります',
'string.empty': '名前を入力してください',
'string.min': '名前は{#limit}文字以上である必要があります',
'string.max': '名前は{#limit}文字以下である必要があります',
'any.required': '名前は必須項目です'
}),
email: Joi.string().email().required().messages({
'string.email': '有効なメールアドレスを入力してください',
'any.required': 'メールアドレスは必須項目です'
}),
age: Joi.number().integer().min(0).max(150).messages({
'number.base': '年齢は数値である必要があります',
'number.integer': '年齢は整数である必要があります',
'number.min': '年齢は{#limit}歳以上である必要があります',
'number.max': '年齢は{#limit}歳以下である必要があります'
})
});
// バリデーションエラーの詳細処理
function handleValidationError(error) {
const errors = {};
error.details.forEach(detail => {
const field = detail.path.join('.');
errors[field] = {
message: detail.message,
value: detail.context.value,
type: detail.type
};
});
return {
success: false,
message: 'バリデーションエラーが発生しました',
errors: errors,
errorCount: error.details.length
};
}
// 使用例
const testData = {
name: 'A', // 短すぎる
email: 'invalid-email', // 無効なメール
age: -5 // 負の値
};
const { error } = detailedSchema.validate(testData, { abortEarly: false });
if (error) {
const errorResponse = handleValidationError(error);
console.log(JSON.stringify(errorResponse, null, 2));
}
// カスタムエラー処理クラス
class ValidationService {
static validate(schema, data, options = {}) {
const defaultOptions = {
abortEarly: false,
allowUnknown: false,
stripUnknown: true,
...options
};
const result = schema.validate(data, defaultOptions);
if (result.error) {
return {
isValid: false,
data: null,
errors: this.formatErrors(result.error),
summary: this.getErrorSummary(result.error)
};
}
return {
isValid: true,
data: result.value,
errors: null,
summary: null
};
}
static formatErrors(error) {
return error.details.reduce((acc, detail) => {
const fieldPath = detail.path.join('.');
acc[fieldPath] = {
message: detail.message,
value: detail.context?.value,
type: detail.type,
constraint: detail.context?.limit || detail.context?.valids
};
return acc;
}, {});
}
static getErrorSummary(error) {
const totalErrors = error.details.length;
const fieldCount = new Set(error.details.map(d => d.path[0])).size;
const errorTypes = error.details.map(d => d.type);
return {
totalErrors,
affectedFields: fieldCount,
mostCommonError: this.getMostCommon(errorTypes),
hasRequiredErrors: errorTypes.includes('any.required'),
hasTypeErrors: errorTypes.some(type => type.includes('.base'))
};
}
static getMostCommon(array) {
const counts = array.reduce((acc, val) => {
acc[val] = (acc[val] || 0) + 1;
return acc;
}, {});
return Object.keys(counts).reduce((a, b) =>
counts[a] > counts[b] ? a : b
);
}
}
// ValidationServiceの使用例
const userValidation = ValidationService.validate(detailedSchema, testData);
console.log('バリデーション結果:', userValidation);
型安全性とTypeScript統合
// TypeScript用の型定義(型安全なJoi使用)
import * as Joi from 'joi';
// スキーマから型を推論するヘルパー
type JoiSchemaType<T> = T extends Joi.ObjectSchema<infer U> ? U : never;
// ユーザースキーマの定義
const userSchemaTyped = Joi.object({
id: Joi.number().integer().positive().required(),
username: Joi.string().alphanum().min(3).max(30).required(),
email: Joi.string().email().required(),
isActive: Joi.boolean().default(true),
profile: Joi.object({
firstName: Joi.string().required(),
lastName: Joi.string().required(),
age: Joi.number().integer().min(18).optional(),
avatar: Joi.string().uri().optional()
}).optional(),
roles: Joi.array().items(Joi.string().valid('admin', 'user', 'moderator')).default(['user']),
metadata: Joi.object().pattern(Joi.string(), Joi.any()).optional(),
createdAt: Joi.date().default(() => new Date()),
lastLogin: Joi.date().optional()
});
// 型の定義
interface User {
id: number;
username: string;
email: string;
isActive: boolean;
profile?: {
firstName: string;
lastName: string;
age?: number;
avatar?: string;
};
roles: Array<'admin' | 'user' | 'moderator'>;
metadata?: { [key: string]: any };
createdAt: Date;
lastLogin?: Date;
}
// 型安全なバリデーション関数
function validateUser(data: unknown): { isValid: true; data: User } | { isValid: false; error: Joi.ValidationError } {
const result = userSchemaTyped.validate(data);
if (result.error) {
return { isValid: false, error: result.error };
}
return { isValid: true, data: result.value as User };
}
// API関数での使用例
async function createUser(userData: unknown): Promise<User> {
const validation = validateUser(userData);
if (!validation.isValid) {
throw new Error(`バリデーションエラー: ${validation.error.message}`);
}
// validation.dataはUser型として型安全
const user = validation.data;
console.log(`ユーザー作成: ${user.username} (${user.email})`);
// データベース保存ロジック
return user;
}
// 汎用バリデーション関数
class TypedValidator<T> {
constructor(private schema: Joi.ObjectSchema<T>) {}
validate(data: unknown): { success: true; data: T } | { success: false; errors: string[] } {
const result = this.schema.validate(data, { abortEarly: false });
if (result.error) {
return {
success: false,
errors: result.error.details.map(detail => detail.message)
};
}
return {
success: true,
data: result.value as T
};
}
async validateAsync(data: unknown): Promise<T> {
const result = await this.schema.validateAsync(data);
return result as T;
}
}
// 型安全なバリデーターの使用
const userValidator = new TypedValidator<User>(userSchemaTyped);
const testUserData = {
id: 1,
username: 'johndoe',
email: '[email protected]',
profile: {
firstName: 'John',
lastName: 'Doe',
age: 30
}
};
const validationResult = userValidator.validate(testUserData);
if (validationResult.success) {
// validationResult.dataはUser型として型安全
console.log(`検証済みユーザー: ${validationResult.data.username}`);
} else {
console.log('エラー:', validationResult.errors);
}
// 複雑なネストしたオブジェクトの型安全バリデーション
interface APIResponse<T> {
success: boolean;
data: T;
message: string;
timestamp: Date;
}
function createAPIResponseSchema<T>(dataSchema: Joi.Schema<T>) {
return Joi.object({
success: Joi.boolean().required(),
data: dataSchema.required(),
message: Joi.string().required(),
timestamp: Joi.date().required()
});
}
const userAPIResponseSchema = createAPIResponseSchema(userSchemaTyped);
type UserAPIResponse = APIResponse<User>;
// 実用的な例:Express.jsでの型安全な使用
/*
import { Request, Response, NextFunction } from 'express';
interface TypedRequest<T> extends Request {
validatedBody: T;
}
function validateBody<T>(schema: Joi.ObjectSchema<T>) {
return (req: Request, res: Response, next: NextFunction) => {
const result = schema.validate(req.body);
if (result.error) {
return res.status(400).json({
success: false,
message: 'バリデーションエラー',
errors: result.error.details.map(d => d.message)
});
}
(req as TypedRequest<T>).validatedBody = result.value as T;
next();
};
}
// 使用例
app.post('/users', validateBody(userSchemaTyped), (req: TypedRequest<User>, res: Response) => {
// req.validatedBodyはUser型として型安全
const user = req.validatedBody;
console.log(`新しいユーザー: ${user.username}`);
res.json({ success: true, data: user });
});
*/