Let's Use `IsEnum` Properly in NestJS with class-validator

Let's Use `IsEnum` Properly in NestJS with class-validator

D
dongAuthor
5 min read

When validating DTOs in NestJS, there are many cases where you need to “allow only specific values”. For example, you may want to allow only 'admin' | 'user' | 'blogger' for the role (role) or 'S' | 'M' | 'L' | 'XL' for product size (size). In such cases, using the @IsEnum decorator from class-validator effectively allows for clean and secure input validation.


Review of Basic Usage of @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 ensures both type safety and data integrity. In other words, if the role value isn’t included in the enum, it will automatically throw a 400 error.


Case-insensitive Validation

In reality, users may send inputs like "Admin", "ADMIN", or "admin". Not accepting these can lead to unnecessary user errors. Here are three patterns to solve this 👇


1️⃣ Convert to Lowercase First with @Transform

The simplest and most practical approach.

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

💡 Pros: Simple implementation, almost no runtime overhead
⚠️ Cons: Enum values must be defined in lowercase

🩼

Tip: Define enum values in lowercase and convert inputs with .toLowerCase() to avoid case sensitivity errors in REST API requests.


2️⃣ Regex-based Validation with @Matches

Useful when enum values are many or change dynamically.

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

💡 Pros: Case-insensitive with i flag
⚠️ Cons: More complex regex, no enum type autocompletion


3️⃣ Create a Custom Decorator: IsEnumCaseInsensitive

This is a truly “production-grade” solution. Instead of repeating conversions or regex, let’s build a reusable validator.

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 'The value is not included in the allowed enum list.';
  }
}

export function IsEnumCaseInsensitive(enumObject: any, validationOptions?: ValidationOptions) {
  return function (object: Object, propertyName: string) {
    registerDecorator({
      target: object.constructor,
      propertyName,
      options: validationOptions,
      constraints: [enumObject],
      validator: IsEnumCaseInsensitiveConstraint,
    });
  };
}

Usage example 👇

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 must be one of: admin, user, blogger (case-insensitive)',
  })
  role: UserRole;
}

💡 Pros: Reusable per enum
⚙️ Cons: Slight boilerplate overhead

💡

Pro Tip: Place shared validation decorators in the src/validators/ directory and import them in all DTOs for easier maintenance.


Pro Tip: Always Enable transform: true Option

When setting up ValidationPipe in NestJS’s main.ts, make sure to enable transform: true.

app.useGlobalPipes(
  new ValidationPipe({
    transform: true,
    whitelist: true,
    forbidNonWhitelisted: true,
  }),
);

This setting is required for @Transform() to work and for proper string-to-enum conversion.

🩼

Note: If `transform` is disabled, both @Transform() and @Type() in DTOs will be ignored, causing enum validation to always fail.


Other Useful Validation Patterns

In addition to @IsEnum, class-validator offers several combinations that make DTOs more robust 👇

Complex String Validation

@IsString()
@MinLength(2)
@MaxLength(30)
@Matches(/^[가-힣a-zA-Z\s]+$/, {
  message: 'Name can only include Korean, English letters, and spaces.',
})
name: string;

Nested Object Validation

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

Object Validation in Arrays

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 vs Type Safety

TypeScript’s enum guarantees compile-time type safety, while @IsEnum validates runtime data. So you need both for real safety.

Item TypeScript Enum @IsEnum
When Compile Time Runtime
Role Type Constraint Actual Request Validation
On Failure Compile Error 400 Bad Request
Let's Use `IsEnum` Properly in NestJS with class-validator | devdong