Nestjsで class‑validator の `IsEnum` を正しく使おう
NestJSでDTO検証をしていると、**「特定の値だけを許可したい場合」**が多くあります。例えば、ロール(role)は 'admin' | 'user' | 'blogger' のどれかだけ、商品サイズ(size)は 'S' | 'M' | 'L' | 'XL' のどれかだけを許可したいときです。こんな場合、class‑validator の @IsEnum デコレータ をきちんと活用すれば、きれいで安全な入力検証を実装できます。
@IsEnum の基本的な使い方をおさらい
// enums/user-role.enum.ts
export enum UserRole {
ADMIN = 'admin',
USER = 'user',
BLOGGER = 'blogger',
}
// dto/create-user.dto.ts
import { IsEnum, IsString, IsNotEmpty, IsEmail } from 'class-validator';
import { UserRole } from '../enums/user-role.enum';
export class CreateUserDto {
@IsString()
@IsNotEmpty()
name: string;
@IsEmail()
email: string;
@IsEnum(UserRole, {
message: 'role must be one of: admin, user, blogger',
})
role: UserRole;
}
@IsEnum は型の安全性とデータの整合性を同時に担保します。つまり、role の値が enum に含まれていなければ、自動的に 400 エラーが発生します。
大文字小文字を区別しない検証 (Case‑insensitive Validation)
実務ではユーザーが "Admin"、"ADMIN"、"admin" といったさまざまな入力をしてきます。これを許可しないと、不要なユーザーエラーを引き起こします。この課題を解決する3つのパターンを紹介します👇
1️⃣ @Transform を使ってあらかじめ小文字変換する
最も簡単かつ実用的な方法です。
import { IsEnum, IsString, IsNotEmpty } from 'class-validator';
import { Transform } from 'class-transformer';
import { UserRole } from '../enums/user-role.enum';
export class CreateUserDto {
@IsString()
@IsNotEmpty()
name: string;
@Transform(({ value }) => value?.toString().toLowerCase())
@IsEnum(UserRole, {
message: 'role must be one of: admin, user, blogger',
})
role: UserRole;
}
💡 長所: 実装が簡単、ランタイムオーバーヘッドほぼなし
⚠️ 短所: enum の値が必ず小文字で定義されていなければならない
Tip: enum の値をすべて小文字で定義し、入力値を `.toLowerCase()` で変換すれば、REST API リクエスト時のケース差異によるエラーを防げます。
2️⃣ @Matches を使った正規表現ベースの検証
enum の値が多かったり、動的に変化する場合に有用です。
import { Matches, IsString } from 'class-validator';
import { UserRole } from '../enums/user-role.enum';
export class CreateUserDto {
@IsString()
@Matches(`^(${Object.values(UserRole).join('|')})$`, 'i', {
message: 'role must be one of: admin, user, blogger (case-insensitive)',
})
role: UserRole;
}
💡 長所: 大文字小文字を無視(
iフラグ)可能
⚠️ 短所: 正規表現の複雑性が増す、enum タイプのオートコンプリート不可
3️⃣ カスタムデコレータを作って IsEnumCaseInsensitive を作成する
本当の「実務用」アプローチです。反復する変換や正規表現の代わりに、再利用可能な検証器を作ってみましょう。
import {
registerDecorator,
ValidationOptions,
ValidatorConstraint,
ValidatorConstraintInterface
} from 'class-validator';
@ValidatorConstraint({ async: false })
export class IsEnumCaseInsensitiveConstraint implements ValidatorConstraintInterface {
validate(value: any, args: any) {
const [enumObject] = args.constraints;
const enumValues = Object.values(enumObject).map(v => v.toString().toLowerCase());
return enumValues.includes(value?.toString().toLowerCase());
}
defaultMessage() {
return '入力値が許可された enum の一覧にありません。';
}
}
export function IsEnumCaseInsensitive(enumObject: any, validationOptions?: ValidationOptions) {
return function (object: Object, propertyName: string) {
registerDecorator({
target: object.constructor,
propertyName,
options: validationOptions,
constraints: [enumObject],
validator: IsEnumCaseInsensitiveConstraint,
});
};
}
使用例 👇
import { Transform } from 'class-transformer';
import { IsEnumCaseInsensitive } from '../validators/is-enum-case-insensitive.decorator';
import { UserRole } from '../enums/user-role.enum';
export class CreateUserDto {
@Transform(({ value }) => value?.toString().toLowerCase())
@IsEnumCaseInsensitive(UserRole, {
message: 'role は admin, user, blogger のいずれかである必要があります(大文字小文字無視)',
})
role: UserRole;
}
💡 長所: enum ごとに再利用可能
⚙️ 短所: ややボイラープレートが存在
実務ヒント: 共用の有効性デコレータを `src/validators/` パスに置き、すべての DTO で呼び出して使えば、保守性が大幅に向上します。
実務ヒント: transform: true オプションは絶対に有効に
NestJS の main.ts で ValidationPipe を設定する際には、必ず transform: true をオンにしてください。
app.useGlobalPipes(
new ValidationPipe({
transform: true,
whitelist: true,
forbidNonWhitelisted: true,
}),
);
この設定があることで @Transform() が適用され、文字列 → enum 変換が正常に動作します。
注意: `transform` をオフにすると DTO の `@Transform()` と `@Type()` が無視され、enum 検証が常に失敗します。
その他の実用的な検証パターン
@IsEnum の他にも、class‑validator には DTO をより堅牢にするための有用な組み合わせがいくつかあります👇
複合文字列検証
@IsString()
@MinLength(2)
@MaxLength(30)
@Matches(/^[가-힣a-zA-Z\s]+$/, {
message: '名前はハングル、英字、空白のみを含めることができます。',
})
name: string;
ネストオブジェクトの検証
import { ValidateNested, Type } from 'class-validator';
class AddressDto {
@IsString()
city: string;
@IsString()
street: string;
}
export class UserWithAddressDto {
@IsString()
name: string;
@ValidateNested()
@Type(() => AddressDto)
address: AddressDto;
}
配列内オブジェクトの検証
import { IsArray, ValidateNested, Type } from 'class-validator';
import { ProductSize } from '../enums/product-size.enum';
class OrderItemDto {
@IsString()
productName: string;
@IsEnum(ProductSize)
size: ProductSize;
}
export class OrderDto {
@IsArray()
@ValidateNested({ each: true })
@Type(() => OrderItemDto)
items: OrderItemDto[];
}
IsEnum と型安全性の比較
TypeScript の enum は コンパイル時 に型を保証し、@IsEnum は ランタイム において実際のデータを検証します。つまり、両方を併用することで本当に安全になります。
| 区分 | TypeScript Enum | @IsEnum |
|---|---|---|
| 時点 | コンパイル時 | ランタイム |
| 役割 | 型の制限 | 実際のリクエスト検証 |
| 失敗時 | コンパイルエラー | 400 Bad Request |