Nestjsで class‑validator の `IsEnum` を正しく使おう

Nestjsで class‑validator の `IsEnum` を正しく使おう

D
dongAuthor
4 min read

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.tsValidationPipe を設定する際には、必ず 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
Nestjsで class‑validator の `IsEnum` を正しく使おう | devdong