class-validator

バリデーションライブラリTypeScriptデコレーターNestJSOOPランタイム

ライブラリ

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
    };
  }
}