class-validator の使い方 — デコレータベースのクラスバリデーション
一言でいうと
class-validator は、TypeScript/JavaScript のクラスプロパティにデコレータを付与するだけでバリデーションを実現するライブラリです。内部的に validator.js を利用しており、ブラウザ・Node.js の両方で動作します。
どんな時に使う?
- NestJS での API リクエストバリデーション — DTO(Data Transfer Object)クラスにデコレータを付けて、コントローラーに到達する前にリクエストボディを自動検証する
- フォーム入力値のサーバーサイドバリデーション — ユーザーから受け取ったデータをクラスインスタンスに変換し、型安全にバリデーションを行う
- ドメインモデルの整合性チェック — エンティティやバリューオブジェクトに制約を宣言的に定義し、ビジネスルールの違反を検出する
インストール
# npm
npm install class-validator --save
# yarn
yarn add class-validator
# pnpm
pnpm add class-validator
注意: TypeScript で使用する場合、
tsconfig.jsonでexperimentalDecoratorsとemitDecoratorMetadataをtrueに設定してください。
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
基本的な使い方
最も典型的なパターンは、クラスのプロパティにバリデーションデコレータを付与し、validate 関数で検証する方法です。
import {
validate,
validateOrReject,
IsString,
IsEmail,
IsInt,
Length,
Min,
Max,
} from 'class-validator';
class CreateUserDto {
@IsString()
@Length(2, 50)
name: string;
@IsEmail()
email: string;
@IsInt()
@Min(0)
@Max(150)
age: number;
}
async function main() {
const dto = new CreateUserDto();
dto.name = 'A'; // 2文字未満なのでエラー
dto.email = 'invalid-email'; // メール形式でないのでエラー
dto.age = 200; // 150を超えているのでエラー
const errors = await validate(dto);
if (errors.length > 0) {
// errors は ValidationError[] 型
errors.forEach(err => {
console.log(`プロパティ: ${err.property}`);
console.log(`エラー内容:`, err.constraints);
});
} else {
console.log('バリデーション成功');
}
}
main();
validateOrReject を使えば、エラー時に例外をスローさせることもできます。
async function createUser(dto: CreateUserDto) {
try {
await validateOrReject(dto);
// バリデーション成功 → ビジネスロジックへ
} catch (errors) {
// errors は ValidationError[] 型
console.log('バリデーション失敗:', errors);
}
}
よく使う API — class-validator の主要機能と使い方
1. 基本的なバリデーションデコレータ
import {
IsString,
IsNumber,
IsBoolean,
IsDate,
IsEmail,
IsUrl,
IsUUID,
IsEnum,
IsOptional,
IsNotEmpty,
} from 'class-validator';
enum Role {
ADMIN = 'admin',
USER = 'user',
}
class UserProfile {
@IsNotEmpty()
@IsString()
name: string;
@IsEmail()
email: string;
@IsUrl()
@IsOptional() // undefined / null の場合はバリデーションをスキップ
website?: string;
@IsEnum(Role)
role: Role;
@IsUUID('4')
id: string;
@IsDate()
createdAt: Date;
@IsBoolean()
isActive: boolean;
}
2. 文字列・数値の制約デコレータ
import {
Length,
MinLength,
MaxLength,
Contains,
Matches,
IsInt,
Min,
Max,
IsPositive,
IsNegative,
} from 'class-validator';
class Product {
@Length(3, 100)
name: string;
@MinLength(10)
@MaxLength(5000)
description: string;
@Matches(/^[A-Z]{2}-\d{4}$/, {
message: 'SKUは「XX-0000」形式で入力してください',
})
sku: string;
@IsInt()
@IsPositive()
@Max(999999)
price: number;
@IsInt()
@Min(0)
stock: number;
}
3. ネストされたオブジェクトのバリデーション
class-transformer と組み合わせて使うのが一般的です。@ValidateNested() と @Type() を併用します。
import { ValidateNested, IsString, IsPostalCode } from 'class-validator';
import { Type } from 'class-transformer';
class Address {
@IsString()
street: string;
@IsString()
city: string;
@IsPostalCode('JP')
postalCode: string;
}
class Order {
@IsString()
orderId: string;
@ValidateNested()
@Type(() => Address) // class-transformer のデコレータ
shippingAddress: Address;
}
// 使用例
async function validateOrder() {
const order = new Order();
order.orderId = 'ORD-001';
const address = new Address();
address.street = '';
address.city = '東京';
address.postalCode = 'invalid';
order.shippingAddress = address;
const errors = await validate(order);
// shippingAddress.street と shippingAddress.postalCode でエラーが発生
// errors[0].children にネストされた ValidationError が格納される
}
4. カスタムバリデーションメッセージ
import { IsString, MinLength, ValidationArguments } from 'class-validator';
class Article {
@IsString({ message: 'タイトルは文字列で入力してください' })
@MinLength(5, {
message: (args: ValidationArguments) => {
return `タイトルは${args.constraints[0]}文字以上必要です(現在: ${args.value?.length ?? 0}文字)`;
},
})
title: string;
}
特殊トークンも利用可能です:
| トークン | 説明 |
|---|---|
$value | バリデーション対象の値 |
$property | プロパティ名 |
$target | クラス名 |
$constraint1, $constraint2... | デコレータに渡した制約値 |
class Post {
@MinLength(10, {
message: '$propertyは最低$constraint1文字必要です(入力値: $value)',
})
title: string;
}
5. カスタムバリデーションデコレータ
プロジェクト固有のバリデーションルールを作成できます。
import {
registerDecorator,
ValidationOptions,
ValidatorConstraint,
ValidatorConstraintInterface,
ValidationArguments,
} from 'class-validator';
// 1. バリデーションロジックを定義
@ValidatorConstraint({ async: false })
class IsJapanesePhoneNumberConstraint implements ValidatorConstraintInterface {
validate(value: any, args: ValidationArguments): boolean {
if (typeof value !== 'string') return false;
return /^0\d{1,4}-?\d{1,4}-?\d{4}$/.test(value);
}
defaultMessage(args: ValidationArguments): string {
return `${args.property}は有効な日本の電話番号形式ではありません`;
}
}
// 2. デコレータファクトリを作成
function IsJapanesePhoneNumber(validationOptions?: ValidationOptions) {
return function (object: Object, propertyName: string) {
registerDecorator({
target: object.constructor,
propertyName: propertyName,
options: validationOptions,
constraints: [],
validator: IsJapanesePhoneNumberConstraint,
});
};
}
// 3. 使用
class ContactForm {
@IsJapanesePhoneNumber({ message: '正しい電話番号を入力してください' })
phone: string;
}
バリデーションオプション(ValidatorOptions)
validate の第2引数に渡せるオプションは実用上非常に重要です。
import { validate } from 'class-validator';
const errors = await validate(dto, {
whitelist: true, // デコレータのないプロパティを自動除去
forbidNonWhitelisted: true, // デコレータのないプロパティがあればエラー
forbidUnknownValues: true, // 未知のオブジェクトを拒否(デフォルト: true)
skipMissingProperties: false, // 欠損プロパティをスキップしない
groups: ['create'], // バリデーショングループを指定
stopAtFirstError: true, // 最初のエラーで停止
validationError: {
target: false, // エラーオブジェクトに target を含めない(API レスポンス向け)
value: false, // エラーオブジェクトに value を含めない
},
});
重要:
forbidUnknownValuesはデフォルトでtrueです。falseにすると未知のオブジェクトがバリデーションを通過してしまうため、変更は推奨されません。
バリデーショングループの使い方
作成時と更新時で異なるルールを適用したい場合に便利です。
import { IsNotEmpty, IsOptional, IsString, MinLength, validate } from 'class-validator';
class UpdateUserDto {
@IsNotEmpty({ groups: ['create'] })
@IsOptional({ groups: ['update'] })
@IsString({ always: true }) // always: true で全グループに適用
@MinLength(2, { always: true })
name: string;
@IsNotEmpty({ groups: ['create'] })
@IsOptional({ groups: ['update'] })
@IsString({ always: true })
email: string;
}
// 作成時: name, email ともに必須
const createErrors = await validate(dto, { groups: ['create'] });
// 更新時: name, email は省略可能
const updateErrors = await validate(dto, { groups: ['update'] });
類似パッケージとの比較
| 特徴 | class-validator | zod | joi | yup |
|---|---|---|---|---|
| アプローチ | デコレータベース(クラス) | スキーマベース(関数型) | スキーマベース(チェーン) | スキーマベース(チェーン) |
| TypeScript 親和性 | ◎(デコレータ前提) | ◎(型推論が強力) | △(別途型定義が必要) | ○ |
| NestJS 統合 | ◎(公式推奨) | △(追加設定が必要) | △ | △ |
| バンドルサイズ | やや大きい | 小さい | 大きい | 中程度 |
| ランタイム型生成 | ✗ | ✓(z.infer) | ✗ | ✗ |
| 非同期バリデーション | ✓ | ✓ | ✓ | ✓ |
| カスタムルール | デコレータで定義 | .refine() | .custom() | .test() |
選定の目安:
- NestJS を使っている → class-validator(公式推奨、class-transformer との連携が強力)
- 関数型スタイルが好み / フロントエンド中心 → zod
- Express + 軽量に使いたい → zod または joi
注意点・Tips
1. プレーンオブジェクトではバリデーションが効かない
// ❌ これではバリデーションされない
const plain = { name: '', email: 'invalid' };
const errors = await validate(plain as any); // エラーは検出されない
// ✅ クラスインスタンスを生成する
import { plainToInstance } from 'class-transformer';
const dto = plainToInstance(CreateUserDto, plain);
const errors = await validate(dto); // 正しくバリデーションされる
これは class-validator がプロトタイプチェーンを参照してデコレータのメタデータを取得するためです。plainToInstance(class-transformer)でインスタンス化するのが定番パターンです。
2. whitelist: true は本番環境で必須
// 悪意のあるリクエストボディ
// { "name": "Alice", "isAdmin": true }
// whitelist: true なら isAdmin は自動的に除去される
const errors = await validate(dto, { whitelist: true });
デコレータが付いていないプロパティを自動除去することで、マスアサインメント攻撃を防げます。
3. API レスポンスに target を含めない
// target にはバリデーション対象のオブジェクト全体が含まれる
// パスワードなどの機密情報が漏洩する可能性がある
const errors = await validate(dto, {
validationError: { target: false },
});