class-validator
ライブラリ
class-validator
概要
class-validatorはTypeScript専用のデコレーターベース・バリデーションライブラリです。クラスのプロパティにデコレーターを適用することで宣言的なバリデーションを実現し、NestJSのデフォルトバリデーターとして採用されています。内部的にvalidator.jsを使用し、Node.jsとブラウザの両方で動作します。2025年現在、TypeScriptの成熟とNestJS人気により、企業向けTypeScriptプロジェクトでの採用が拡大しており、オブジェクト指向プログラミングとの親和性が高いバリデーションソリューションです。
詳細
class-validator 0.14系は2025年現在の最新安定版で、TypeScriptデコレーターの力を最大限活用したバリデーションライブラリです。内部的にvalidator.jsライブラリを使用し、豊富な組み込みバリデーション関数を提供します。NestJSフレームワークでの標準採用により、企業向けNode.jsアプリケーション開発において重要な地位を占めています。class-transformerライブラリとの連携により、プレーンオブジェクトからクラスインスタンスへの変換とバリデーションを統合的に実行可能。
主な特徴
- デコレーターベース: TypeScriptデコレーターによる宣言的バリデーション
- 型安全性: TypeScriptとの完全統合による強力な型サポート
- NestJS統合: NestJSでの標準採用による企業級アプリケーション対応
- 豊富な組み込み制約: @IsEmail、@IsNumber等60以上のバリデーター
- カスタムバリデーター: 独自のビジネスロジックに対応する拡張性
- ネストバリデーション: オブジェクトの階層構造に対応したバリデーション
メリット・デメリット
メリット
- TypeScript開発者にとって自然で直感的なAPI
- NestJSエコシステムでの標準的地位による安定性
- オブジェクト指向プログラミングとの高い親和性
- class-transformerとの統合による強力なデータ処理
- 詳細なエラー情報とカスタマイズ性
- Node.jsとブラウザでのクロスプラットフォーム対応
デメリット
- TypeScript/JavaScript専用でクロスランゲージ対応なし
- デコレーターへの依存により設定の複雑性
- バリデーションクラスの定義が必要で冗長性
- ランタイムでのパフォーマンスオーバーヘッド
- 関数型プログラミングスタイルには適さない
- 大規模プロジェクトでのクラス管理の複雑化
参考ページ
書き方の例
インストールと基本セットアップ
# class-validatorのインストール
npm install class-validator
yarn add class-validator
pnpm add class-validator
# reflect-metadataが必要(TypeScriptデコレーター用)
npm install reflect-metadata
# class-transformerとの併用(推奨)
npm install class-transformer
# TypeScript設定必須
# tsconfig.json
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
基本的なクラスバリデーション
import {
validate,
validateOrReject,
Contains,
IsInt,
Length,
IsEmail,
IsFQDN,
IsDate,
Min,
Max,
IsOptional,
IsString,
IsNumber,
IsBoolean
} from 'class-validator';
// エントリーポイントでreflect-metadataをインポート
import 'reflect-metadata';
export class User {
@Length(10, 20, {
message: 'タイトルは10文字以上20文字以下である必要があります'
})
title: string;
@Contains('hello', {
message: 'テキストに"hello"が含まれている必要があります'
})
text: string;
@IsInt({ message: '評価は整数である必要があります' })
@Min(0, { message: '評価は0以上である必要があります' })
@Max(10, { message: '評価は10以下である必要があります' })
rating: number;
@IsEmail({}, { message: '有効なメールアドレスを入力してください' })
email: string;
@IsFQDN({}, { message: '有効なドメイン名を入力してください' })
site: string;
@IsDate({ message: '有効な日付を入力してください' })
createDate: Date;
@IsOptional()
@IsString({ message: '説明は文字列である必要があります' })
description?: string;
constructor() {
this.title = '';
this.text = '';
this.rating = 0;
this.email = '';
this.site = '';
this.createDate = new Date();
}
}
// バリデーション実行例
async function validateUser() {
const user = new User();
user.title = 'Hello'; // 10文字未満でエラー
user.text = 'world'; // 'hello'が含まれていないのでエラー
user.rating = 15; // 10を超えているのでエラー
user.email = 'invalid-email'; // 無効なメール形式
user.site = 'invalid-domain'; // 無効なドメイン
user.createDate = new Date();
try {
const errors = await validate(user);
if (errors.length > 0) {
console.log('バリデーションエラー:');
errors.forEach(error => {
console.log(`プロパティ: ${error.property}`);
console.log(`値: ${error.value}`);
console.log(`制約:`, error.constraints);
console.log('---');
});
} else {
console.log('バリデーション成功');
}
} catch (errors) {
console.log('バリデーション例外:', errors);
}
}
// validateOrRejectを使用した例外処理
async function validateUserWithException() {
const user = new User();
// 無効なデータを設定
try {
await validateOrReject(user);
console.log('バリデーション成功');
} catch (errors) {
console.log('バリデーションエラー:', errors);
}
}
ネストオブジェクトと配列のバリデーション
import {
ValidateNested,
IsArray,
ArrayMinSize,
ArrayMaxSize,
ArrayNotEmpty,
IsInstance,
Type
} from 'class-validator';
export class Address {
@Length(1, 50, { message: '街道名は1文字以上50文字以下である必要があります' })
street: string;
@Length(1, 50, { message: '都市名は1文字以上50文字以下である必要があります' })
city: string;
@Length(1, 50, { message: '州名は1文字以上50文字以下である必要があります' })
state: string;
@Length(5, 10, { message: '郵便番号は5文字以上10文字以下である必要があります' })
zipCode: string;
constructor() {
this.street = '';
this.city = '';
this.state = '';
this.zipCode = '';
}
}
export class Tag {
@Length(1, 20, { message: 'タグ名は1文字以上20文字以下である必要があります' })
name: string;
@IsOptional()
@IsString({ message: '色は文字列である必要があります' })
color?: string;
constructor(name: string = '', color?: string) {
this.name = name;
this.color = color;
}
}
export class Post {
@Length(10, 20)
title: string;
@Contains('hello')
text: string;
@IsInt()
@Min(0)
@Max(10)
rating: number;
// ネストしたオブジェクトのバリデーション
@ValidateNested()
@IsInstance(Address, { message: '住所は有効なAddressインスタンスである必要があります' })
@Type(() => Address)
address: Address;
// 配列要素のバリデーション
@IsArray({ message: 'タグは配列である必要があります' })
@ValidateNested({ each: true })
@ArrayMinSize(1, { message: 'タグは最低1つ必要です' })
@ArrayMaxSize(5, { message: 'タグは最大5つまでです' })
@Type(() => Tag)
tags: Tag[];
// プリミティブ配列のバリデーション
@IsArray()
@ArrayNotEmpty({ message: 'カテゴリは空ではいけません' })
@IsString({ each: true, message: '各カテゴリは文字列である必要があります' })
categories: string[];
constructor() {
this.title = '';
this.text = '';
this.rating = 0;
this.address = new Address();
this.tags = [];
this.categories = [];
}
}
// 複雑なオブジェクトのバリデーション例
async function validateComplexObject() {
const post = new Post();
post.title = 'Hello World!';
post.text = 'This is a hello world post';
post.rating = 8;
// 住所の設定
post.address.street = '123 Main St';
post.address.city = 'Tokyo';
post.address.state = 'Tokyo';
post.address.zipCode = '100-0001';
// タグの設定
post.tags = [
new Tag('programming', 'blue'),
new Tag('typescript', 'green')
];
post.categories = ['tech', 'programming'];
const errors = await validate(post);
if (errors.length === 0) {
console.log('すべてのバリデーションが成功しました');
} else {
console.log('バリデーションエラー:', errors);
}
}
NestJS統合とDTOバリデーション
// NestJSでのDTO(Data Transfer Object)定義
import { IsString, IsNumber, IsEmail, IsOptional, Min, Max } from 'class-validator';
import { Transform, Type } from 'class-transformer';
export class CreateUserDto {
@IsString({ message: '名前は文字列である必要があります' })
@Length(2, 50, { message: '名前は2文字以上50文字以下である必要があります' })
name: string;
@IsEmail({}, { message: '有効なメールアドレスを入力してください' })
email: string;
@IsNumber({}, { message: '年齢は数値である必要があります' })
@Min(18, { message: '年齢は18歳以上である必要があります' })
@Max(120, { message: '年齢は120歳以下である必要があります' })
@Type(() => Number) // 文字列から数値への変換
age: number;
@IsOptional()
@IsString()
@Transform(({ value }) => value?.trim()) // 前後の空白を削除
bio?: string;
}
export class UpdateUserDto {
@IsOptional()
@IsString({ message: '名前は文字列である必要があります' })
@Length(2, 50, { message: '名前は2文字以上50文字以下である必要があります' })
name?: string;
@IsOptional()
@IsEmail({}, { message: '有効なメールアドレスを入力してください' })
email?: string;
@IsOptional()
@IsNumber({}, { message: '年齢は数値である必要があります' })
@Min(18, { message: '年齢は18歳以上である必要があります' })
@Max(120, { message: '年齢は120歳以下である必要があります' })
@Type(() => Number)
age?: number;
}
// NestJS Controller
import { Controller, Post, Body, Put, Param, ParseIntPipe } from '@nestjs/common';
import { ValidationPipe } from '@nestjs/common';
@Controller('users')
export class UsersController {
@Post()
async createUser(@Body() createUserDto: CreateUserDto) {
// バリデーションは自動的に実行される
console.log('受信したデータ:', createUserDto);
return { message: 'ユーザーが作成されました', user: createUserDto };
}
@Put(':id')
async updateUser(
@Param('id', ParseIntPipe) id: number,
@Body() updateUserDto: UpdateUserDto
) {
console.log(`ユーザー ${id} を更新:`, updateUserDto);
return { message: 'ユーザーが更新されました', user: updateUserDto };
}
}
// NestJS main.ts でのグローバルバリデーション設定
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe({
whitelist: true, // DTOに定義されていないプロパティを除去
forbidNonWhitelisted: true, // 未定義プロパティがある場合エラー
transform: true, // 自動的にDTOインスタンスに変換
disableErrorMessages: false, // エラーメッセージを表示
validationError: {
target: false, // エラーレスポンスにターゲットオブジェクトを含めない
value: false // エラーレスポンスに値を含めない
}
}));
await app.listen(3000);
}
bootstrap();
カスタムバリデーターと高度なバリデーション
import {
registerDecorator,
ValidationOptions,
ValidatorConstraint,
ValidatorConstraintInterface,
ValidationArguments,
Validate,
IsDateString
} from 'class-validator';
// カスタムバリデーター:パスワード強度チェック
@ValidatorConstraint({ name: 'isStrongPassword', async: false })
export class IsStrongPasswordConstraint implements ValidatorConstraintInterface {
validate(password: string, args: ValidationArguments) {
if (!password) return false;
// 8文字以上、大文字、小文字、数字、特殊文字を含む
const strongPasswordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/;
return strongPasswordRegex.test(password);
}
defaultMessage(args: ValidationArguments) {
return 'パスワードは8文字以上で、大文字、小文字、数字、特殊文字を含む必要があります';
}
}
// カスタムデコレーターの作成
export function IsStrongPassword(validationOptions?: ValidationOptions) {
return function (object: Object, propertyName: string) {
registerDecorator({
target: object.constructor,
propertyName: propertyName,
options: validationOptions,
constraints: [],
validator: IsStrongPasswordConstraint,
});
};
}
// 非同期バリデーター:ユーザー名の重複チェック
@ValidatorConstraint({ name: 'isUserNameUnique', async: true })
export class IsUserNameUniqueConstraint implements ValidatorConstraintInterface {
async validate(userName: string, args: ValidationArguments) {
// 実際のアプリケーションでは、データベースをチェック
const existingUsers = ['admin', 'user', 'test']; // 模擬データ
return new Promise((resolve) => {
setTimeout(() => {
resolve(!existingUsers.includes(userName.toLowerCase()));
}, 100); // 非同期処理をシミュレート
});
}
defaultMessage(args: ValidationArguments) {
return 'ユーザー名 "$value" は既に使用されています';
}
}
export function IsUserNameUnique(validationOptions?: ValidationOptions) {
return function (object: Object, propertyName: string) {
registerDecorator({
target: object.constructor,
propertyName: propertyName,
options: validationOptions,
constraints: [],
validator: IsUserNameUniqueConstraint,
});
};
}
// 条件付きバリデーション
@ValidatorConstraint({ name: 'isMatchingPassword', async: false })
export class IsMatchingPasswordConstraint implements ValidatorConstraintInterface {
validate(confirmPassword: string, args: ValidationArguments) {
const object = args.object as any;
return confirmPassword === object.password;
}
defaultMessage(args: ValidationArguments) {
return 'パスワードが一致しません';
}
}
export function IsMatchingPassword(validationOptions?: ValidationOptions) {
return function (object: Object, propertyName: string) {
registerDecorator({
target: object.constructor,
propertyName: propertyName,
options: validationOptions,
constraints: [],
validator: IsMatchingPasswordConstraint,
});
};
}
// 高度なバリデーションを使用するクラス
export class AdvancedUser {
@IsString({ message: 'ユーザー名は文字列である必要があります' })
@Length(3, 20, { message: 'ユーザー名は3文字以上20文字以下である必要があります' })
@IsUserNameUnique({ message: 'このユーザー名は既に使用されています' })
userName: string;
@IsEmail({}, { message: '有効なメールアドレスを入力してください' })
email: string;
@IsStrongPassword({ message: 'より強固なパスワードを設定してください' })
password: string;
@IsString({ message: 'パスワード確認は文字列である必要があります' })
@IsMatchingPassword({ message: 'パスワードが一致しません' })
confirmPassword: string;
@IsOptional()
@IsDateString({}, { message: '有効な日付形式で入力してください (YYYY-MM-DD)' })
birthDate?: string;
constructor() {
this.userName = '';
this.email = '';
this.password = '';
this.confirmPassword = '';
}
}
// カスタムバリデーターのテスト
async function testAdvancedValidation() {
const user = new AdvancedUser();
user.userName = 'newuser';
user.email = '[email protected]';
user.password = 'StrongPass123!';
user.confirmPassword = 'StrongPass123!';
user.birthDate = '1990-01-01';
try {
const errors = await validate(user);
if (errors.length === 0) {
console.log('高度なバリデーション成功');
} else {
console.log('バリデーションエラー:', errors);
}
} catch (error) {
console.error('バリデーション例外:', error);
}
}
エラーハンドリングとメッセージカスタマイズ
import { validate, ValidationError } from 'class-validator';
// エラー情報の詳細解析
function formatValidationErrors(errors: ValidationError[]): any {
const formattedErrors: any = {};
errors.forEach(error => {
const property = error.property;
if (error.constraints) {
formattedErrors[property] = Object.values(error.constraints);
}
// ネストしたエラーの処理
if (error.children && error.children.length > 0) {
formattedErrors[property] = {
...formattedErrors[property],
children: formatValidationErrors(error.children)
};
}
});
return formattedErrors;
}
// カスタムエラーレスポンス生成
class ValidationErrorResponse {
message: string;
statusCode: number;
errors: any;
timestamp: string;
constructor(errors: ValidationError[]) {
this.message = 'バリデーションエラーが発生しました';
this.statusCode = 400;
this.errors = formatValidationErrors(errors);
this.timestamp = new Date().toISOString();
}
}
// グローバルエラーハンドラー(Express.js用)
import { Request, Response, NextFunction } from 'express';
export function validationErrorHandler(
error: any,
req: Request,
res: Response,
next: NextFunction
) {
if (error instanceof Array && error[0] instanceof ValidationError) {
const validationResponse = new ValidationErrorResponse(error);
return res.status(400).json(validationResponse);
}
next(error);
}
// バリデーション関数のラッパー
export async function validateAndFormat<T>(
object: T,
options?: {
skipMissingProperties?: boolean;
whitelist?: boolean;
forbidNonWhitelisted?: boolean;
}
): Promise<{ isValid: boolean; errors?: any; data?: T }> {
try {
const errors = await validate(object as any, options);
if (errors.length > 0) {
return {
isValid: false,
errors: formatValidationErrors(errors)
};
}
return {
isValid: true,
data: object
};
} catch (error) {
return {
isValid: false,
errors: { global: ['予期しないエラーが発生しました'] }
};
}
}
// 使用例
async function handleUserRegistration(userData: any) {
const user = Object.assign(new AdvancedUser(), userData);
const result = await validateAndFormat(user);
if (!result.isValid) {
console.log('バリデーション失敗:', result.errors);
return { success: false, errors: result.errors };
}
console.log('バリデーション成功:', result.data);
return { success: true, user: result.data };
}
// 部分バリデーション(特定のプロパティのみ)
async function validateSpecificProperty<T>(
object: T,
propertyName: string
): Promise<string[]> {
const errors = await validate(object as any, {
skipMissingProperties: true
});
const propertyErrors = errors.find(error => error.property === propertyName);
if (propertyErrors && propertyErrors.constraints) {
return Object.values(propertyErrors.constraints);
}
return [];
}
// リアルタイムバリデーション(フォーム用)
export class FormValidator<T> {
private object: T;
private fieldErrors: Map<string, string[]> = new Map();
constructor(objectType: new () => T) {
this.object = new objectType();
}
async validateField(fieldName: string, value: any): Promise<string[]> {
(this.object as any)[fieldName] = value;
const errors = await validateSpecificProperty(this.object, fieldName);
this.fieldErrors.set(fieldName, errors);
return errors;
}
getFieldErrors(fieldName: string): string[] {
return this.fieldErrors.get(fieldName) || [];
}
async validateAll(): Promise<{ isValid: boolean; errors: Map<string, string[]> }> {
const errors = await validate(this.object as any);
this.fieldErrors.clear();
errors.forEach(error => {
if (error.constraints) {
this.fieldErrors.set(error.property, Object.values(error.constraints));
}
});
return {
isValid: errors.length === 0,
errors: this.fieldErrors
};
}
}