Fix: NestJS ValidationPipe Not Working — class-validator Decorators Ignored
Quick Answer
How to fix NestJS ValidationPipe not validating requests — global pipe setup, class-transformer, whitelist and transform options, custom validators, and DTO inheritance issues.
The Problem
NestJS ValidationPipe doesn’t reject invalid request bodies:
// DTO
export class CreateUserDto {
@IsEmail()
email: string;
@MinLength(8)
password: string;
}
// Controller
@Post()
async create(@Body() dto: CreateUserDto) {
// Receives the body even with invalid email or short password
// Validation decorators are ignored
}Or validation works but the body contains extra fields that should be stripped:
// Request body: { email: '[email protected]', password: 'secret123', isAdmin: true }
// dto.isAdmin exists even though it's not in the DTO — should be strippedOr a DTO property transforms incorrectly:
@IsInt()
age: number;
// POST body: { "age": "25" }
// Error: age must be an integer — even though the string "25" should convert to 25Or a nested DTO doesn’t validate:
export class CreateOrderDto {
@ValidateNested()
@Type(() => AddressDto)
address: AddressDto; // AddressDto fields not validated
}Why This Happens
NestJS ValidationPipe uses class-validator for validation and class-transformer for type transformation. Several configuration gaps cause validation to silently skip:
ValidationPipenot registered globally or per-route — withoutapp.useGlobalPipes(new ValidationPipe()), the pipe is never applied.class-transformernot installed —transform: truerequiresclass-transformeras a dependency.- Plain objects instead of class instances —
class-validatordecorators only work on class instances. Iftransform: trueisn’t enabled, NestJS passes plain objects (no class information), so decorators are ignored. - Missing
@Type()decorator for nested DTOs —@ValidateNested()alone doesn’t transform nested objects.@Type(() => NestedDto)fromclass-transformeris required. emitDecoratorMetadatanot enabled — TypeScript must emit decorator metadata for NestJS to read type information from class properties.
Fix 1: Register ValidationPipe Globally
The most common cause — ValidationPipe must be explicitly registered:
// main.ts — register globally (recommended)
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, // Strip properties not in the DTO
forbidNonWhitelisted: true, // Throw error if extra properties sent
transform: true, // Auto-transform payloads to DTO instances
transformOptions: {
enableImplicitConversion: true, // Convert string '25' to number 25
},
}),
);
await app.listen(3000);
}
bootstrap();Per-controller or per-route (less common):
// Controller-level
@UsePipes(new ValidationPipe({ whitelist: true }))
@Controller('users')
export class UsersController {}
// Route-level
@Post()
@UsePipes(new ValidationPipe({ whitelist: true, transform: true }))
async create(@Body() dto: CreateUserDto) {}Module-level registration (for dependency injection in custom validators):
// app.module.ts
import { APP_PIPE } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
@Module({
providers: [
{
provide: APP_PIPE,
useValue: new ValidationPipe({
whitelist: true,
transform: true,
}),
},
],
})
export class AppModule {}Note:
app.useGlobalPipes()doesn’t integrate with NestJS’s dependency injection system. For custom validators that inject services (e.g., checking if email is already taken in the database), use theAPP_PIPEprovider approach.
Fix 2: Install Required Packages
ValidationPipe with transform: true requires both class-validator and class-transformer:
npm install class-validator class-transformer
# Verify versions are compatible
npm list class-validator class-transformertsconfig.json — enable decorator metadata:
{
"compilerOptions": {
"experimentalDecorators": true, // Required for decorators
"emitDecoratorMetadata": true, // Required for NestJS type reflection
"strictPropertyInitialization": false // Allow uninitialized class properties in DTOs
}
}Without emitDecoratorMetadata: true, TypeScript doesn’t emit type metadata, and class-transformer can’t perform automatic type conversion.
Fix 3: Fix Nested DTO Validation
@ValidateNested() alone doesn’t transform nested objects into class instances. Add @Type() from class-transformer:
import { Type } from 'class-transformer';
import { ValidateNested, IsString, IsNumber, IsArray } from 'class-validator';
export class AddressDto {
@IsString()
street: string;
@IsString()
city: string;
@IsString()
country: string;
}
export class CreateOrderDto {
@ValidateNested()
@Type(() => AddressDto) // ← Required: tells class-transformer to instantiate AddressDto
address: AddressDto;
// Array of nested objects
@IsArray()
@ValidateNested({ each: true }) // each: true validates every item in the array
@Type(() => OrderItemDto)
items: OrderItemDto[];
}Without @Type() — validation silently skips nested objects:
// WRONG — @ValidateNested without @Type
export class CreateOrderDto {
@ValidateNested()
address: AddressDto; // AddressDto fields NOT validated — plain object passed through
}
// CORRECT
export class CreateOrderDto {
@ValidateNested()
@Type(() => AddressDto)
address: AddressDto; // AddressDto fields ARE validated
}Fix 4: Handle Type Transformation
transform: true converts plain request body values to class instances. enableImplicitConversion: true additionally converts primitive types:
export class PaginationDto {
@IsOptional()
@IsInt()
@Min(1)
page?: number;
@IsOptional()
@IsInt()
@Min(1)
@Max(100)
limit?: number;
}
// Query string: /users?page=2&limit=10
// Without enableImplicitConversion: page and limit are strings '2', '10' — validation fails
// With enableImplicitConversion: page and limit become numbers 2, 10 — validation passes
// In main.ts:
app.useGlobalPipes(new ValidationPipe({
transform: true,
transformOptions: {
enableImplicitConversion: true, // Strings → numbers/booleans based on TS type
},
}));Manual transformation with @Transform():
import { Transform } from 'class-transformer';
import { IsBoolean, IsDate } from 'class-validator';
export class FilterDto {
// Transform string 'true'/'false' to boolean
@Transform(({ value }) => value === 'true' || value === true)
@IsBoolean()
includeDeleted?: boolean;
// Transform ISO string to Date object
@Transform(({ value }) => value ? new Date(value) : undefined)
@IsDate()
@IsOptional()
startDate?: Date;
// Trim and lowercase string
@Transform(({ value }) => value?.trim().toLowerCase())
@IsString()
email?: string;
}Fix 5: Configure whitelist and forbidNonWhitelisted
whitelist: true strips properties not declared in the DTO. forbidNonWhitelisted: true throws an error instead of silently stripping:
// DTO — only declares email and password
export class LoginDto {
@IsEmail()
email: string;
@IsString()
password: string;
}
// Request body: { email: '[email protected]', password: 'secret', rememberMe: true }
// whitelist: false (default) — dto.rememberMe = true (extra field present)
// whitelist: true — dto.rememberMe undefined (stripped silently)
// forbidNonWhitelisted: true — 400 error: "property rememberMe should not exist"// Recommended production config:
app.useGlobalPipes(new ValidationPipe({
whitelist: true, // Strip extra fields
forbidNonWhitelisted: true, // Error on extra fields (catches typos in field names)
transform: true,
}));Allowlist specific non-DTO properties — if you need to pass through extra fields selectively, restructure the DTO:
// Can't use forbidNonWhitelisted if accepting arbitrary metadata
export class CreateEventDto {
@IsString()
name: string;
// Allow additional properties by typing explicitly
@IsObject()
@IsOptional()
metadata?: Record<string, unknown>;
// metadata will be validated as object but its keys won't be checked
}Fix 6: Write Custom Validators
For business logic validation (e.g., check if email is already registered), create custom validators:
import {
ValidatorConstraint,
ValidatorConstraintInterface,
ValidationArguments,
registerDecorator,
ValidationOptions,
} from 'class-validator';
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './user.entity';
// Custom constraint — can inject NestJS services
@ValidatorConstraint({ name: 'isEmailUnique', async: true })
@Injectable()
export class IsEmailUniqueConstraint implements ValidatorConstraintInterface {
constructor(
@InjectRepository(User)
private userRepository: Repository<User>,
) {}
async validate(email: string): Promise<boolean> {
const user = await this.userRepository.findOne({ where: { email } });
return !user; // Return true if valid (email not taken)
}
defaultMessage(args: ValidationArguments): string {
return 'Email $value is already registered';
}
}
// Custom decorator
export function IsEmailUnique(options?: ValidationOptions) {
return function (object: object, propertyName: string) {
registerDecorator({
target: object.constructor,
propertyName,
options,
constraints: [],
validator: IsEmailUniqueConstraint,
});
};
}// DTO using the custom validator
export class RegisterDto {
@IsEmail()
@IsEmailUnique() // Async — checks database
email: string;
@MinLength(8)
password: string;
}// app.module.ts — register the constraint for dependency injection
// (Required for custom validators that inject services)
@Module({
providers: [
IsEmailUniqueConstraint, // Register the constraint
{
provide: APP_PIPE,
useFactory: () => new ValidationPipe({
whitelist: true,
transform: true,
}),
},
],
})
export class AppModule {}Fix 7: Handle Validation Errors
By default, ValidationPipe returns a 400 with all validation errors. Customize the response format:
import { BadRequestException, ValidationPipe } from '@nestjs/common';
import { ValidationError } from 'class-validator';
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
transform: true,
exceptionFactory: (errors: ValidationError[]) => {
// Flatten nested errors into a simple key-message object
const messages = errors.reduce((acc, error) => {
Object.values(error.constraints || {}).forEach(message => {
acc[error.property] = message;
});
return acc;
}, {} as Record<string, string>);
return new BadRequestException({
statusCode: 400,
error: 'Validation Failed',
messages,
});
},
}),
);
// Response format:
// {
// "statusCode": 400,
// "error": "Validation Failed",
// "messages": {
// "email": "email must be an email",
// "password": "password must be longer than or equal to 8 characters"
// }
// }Default NestJS validation error format:
{
"statusCode": 400,
"message": [
"email must be an email",
"password must be longer than or equal to 8 characters"
],
"error": "Bad Request"
}Still Not Working?
DTO class not imported correctly — if your DTO file has a typo or circular import, NestJS silently uses the wrong type. Check that @Body() dto: CreateUserDto imports CreateUserDto from the correct file.
@Body() without a DTO type — @Body() without a type annotation gives a plain object. @Body() body: any or @Body() body: object disables validation entirely. Always specify the DTO class.
Validation only for @Body() decorators — @Param(), @Query() don’t validate with class-validator by default unless the type is a DTO class. For query param validation, use a DTO class:
// CORRECT — validates query params using a DTO class
@Get()
async findAll(@Query() query: PaginationDto) {}
// Wrong — @Query('page') with a plain type doesn't validate
@Get()
async findAll(@Query('page') page: number) {} // page is a string — no validationskipMissingProperties: true — if set, fields without a value are not validated. This silently allows empty required fields. Only use it with @IsOptional() decorators, not as a global bypass.
For related NestJS issues, see Fix: NestJS Circular Dependency and Fix: NestJS Guard Not Working.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: NestJS Swagger UI Not Showing — /api-docs Returns 404 or Blank Page
How to fix NestJS Swagger UI not displaying — SwaggerModule setup, DocumentBuilder, decorators not appearing, guards blocking the docs route, and Fastify vs Express differences.
Fix: NestJS Nest can't resolve dependencies — Provider Not Found Error
How to fix NestJS dependency injection errors — module imports, provider exports, circular dependencies, dynamic modules, and the most common 'can't resolve dependencies' patterns.
Fix: NestJS Guard Not Working — canActivate Always Passes or Is Never Called
How to fix NestJS guards not working — applying guards globally vs controller vs method level, JWT AuthGuard, metadata with Reflector, public routes, and guard execution order.
Fix: NestJS Interceptor Not Triggered — Interceptors Not Running
How to fix NestJS interceptors not being called — global vs controller vs method binding, response transformation, async interceptors, execution context, and interceptor ordering.