How to Validate Nested Objects in NestJS with class-validator: Fixing @Type Decorator Issues
Validation is a critical aspect of building robust APIs, ensuring that incoming data adheres to expected formats and constraints. In NestJS, the class-validator library simplifies this process by allowing you to decorate class properties with validation rules. However, validating nested objects (e.g., a User DTO containing an Address object) can be tricky. A common pain point is getting the @Type decorator from class-transformer to work correctly, as it plays a pivotal role in transforming and validating nested structures.
This blog will demystify nested object validation in NestJS. We’ll explore why @Type is essential, common issues developers face with it, and provide a step-by-step guide to fixing these issues. By the end, you’ll confidently validate even the most complex nested data structures.
Table of Contents#
- Prerequisites
- Understanding Nested Object Validation in NestJS
- The Role of
class-transformerand@Type - Common Issues with the
@TypeDecorator - Step-by-Step Guide to Fixing
@TypeIssues - Advanced Scenarios: Arrays, Optional Objects, and More
- Troubleshooting Tips
- Conclusion
- References
Prerequisites#
Before diving in, ensure you have:
- Basic familiarity with NestJS (controllers, DTOs, and dependency injection).
- A NestJS project set up (use
nest new project-nameif starting fresh). - Installed required dependencies:
npm install class-validator class-transformer
Understanding Nested Object Validation in NestJS#
In NestJS, Data Transfer Objects (DTOs) are classes that define the structure of incoming data. For simple flat DTOs (e.g., a User with name and email), class-validator works out of the box. However, when DTOs contain nested objects (e.g., User with an address property of type Address), validation fails by default.
Why? Because NestJS uses class-transformer under the hood to convert incoming JSON payloads into class instances. Without explicit configuration, nested objects remain plain JavaScript objects (not instances of their DTO classes), and class-validator cannot apply validation rules to their properties.
The Role of class-transformer and @Type#
class-transformer is a library that transforms plain objects into class instances (and vice versa). For nested objects, the @Type decorator from class-transformer is critical: it tells class-transformer the type of the nested property, ensuring it instantiates the correct class.
Example: Why @Type Matters#
Suppose we have a CreateUserDto with a nested address property:
// create-user.dto.ts
import { IsString } from 'class-validator';
export class AddressDto {
@IsString()
street: string;
@IsString()
city: string;
}
export class CreateUserDto {
@IsString()
name: string;
// Nested object (problem: no @Type decorator!)
address: AddressDto;
} Without @Type, class-transformer leaves address as a plain object (not an AddressDto instance). class-validator will not validate address.street or address.city because it only validates class instances, not plain objects.
Common Issues with the @Type Decorator#
Even when using @Type, validation may fail due to subtle mistakes. Let’s explore the most common issues:
1. Forgetting to Import @Type#
The @Type decorator is not part of class-validator—it comes from class-transformer. Forgetting to import it (or importing it from the wrong package) will cause runtime errors.
Fix: Always import @Type from class-transformer:
import { Type } from 'class-transformer'; // Correct 2. Incorrect Type Specification#
@Type requires a function that returns the target class (not the class itself). This avoids issues with circular dependencies and ensures the class is resolved at runtime.
Common Mistake:
@Type(AddressDto) // ❌ Passing the class directly
address: AddressDto; Fix: Use a lambda to return the class:
@Type(() => AddressDto) // ✅ Correct: function returning the class
address: AddressDto; 3. Missing @ValidateNested Decorator#
@Type transforms the nested object into a class instance, but class-validator still needs explicit instruction to validate it. The @ValidateNested decorator (from class-validator) triggers validation for nested objects.
Fix: Pair @Type with @ValidateNested:
import { ValidateNested } from 'class-validator';
export class CreateUserDto {
// ...
@ValidateNested() // Triggers validation for the nested object
@Type(() => AddressDto) // Transforms to AddressDto instance
address: AddressDto;
} 4. Nested Arrays Not Being Transformed#
For arrays of nested objects (e.g., addresses: AddressDto[]), @Type alone won’t transform each element. You need to combine it with @IsArray() and @ValidateNested({ each: true }).
Common Mistake:
@Type(() => AddressDto)
addresses: AddressDto[]; // ❌ Array elements remain plain objects Fix:
import { IsArray, ValidateNested } from 'class-validator';
@IsArray() // Ensures the value is an array
@ValidateNested({ each: true }) // Validates EACH element in the array
@Type(() => AddressDto) // Transforms EACH element to AddressDto
addresses: AddressDto[]; 5. Misconfigured ValidationPipe#
NestJS uses ValidationPipe to apply validation. If the pipe isn’t configured to transform incoming data, class-transformer (and thus @Type) won’t run.
Common Mistake: Missing transform: true in ValidationPipe options:
// main.ts
app.useGlobalPipes(new ValidationPipe()); // ❌ No transformation Fix: Enable transformation in ValidationPipe:
app.useGlobalPipes(
new ValidationPipe({
transform: true, // ✅ Enables class-transformer
whitelist: true, // Optional: strips unvalidated properties
}),
); 6. Nested DTOs Lack Validation Decorators#
Even if @Type and @ValidateNested are correct, validation will fail if the nested DTO itself has no validation rules.
Common Mistake:
// address.dto.ts (missing validation decorators!)
export class AddressDto {
street: string; // ❌ No @IsString()
city: string; // ❌ No @IsString()
} Fix: Always decorate nested DTO properties with class-validator rules:
export class AddressDto {
@IsString() // ✅
street: string;
@IsString() // ✅
city: string;
} Step-by-Step Guide to Fixing @Type Issues#
Let’s walk through a complete example to validate nested objects, fixing the issues above. We’ll build a CreateUserDto with a nested AddressDto and ensure validation works end-to-end.
Step 1: Define Nested DTOs with Validation Rules#
First, create the nested AddressDto and parent CreateUserDto, ensuring all properties have class-validator decorators.
// src/users/dto/address.dto.ts
import { IsString, IsNumber } from 'class-validator';
export class AddressDto {
@IsString() // Validate street is a string
street: string;
@IsString() // Validate city is a string
city: string;
@IsNumber() // Validate zip code is a number
zipCode: number;
} // src/users/dto/create-user.dto.ts
import { IsString, ValidateNested } from 'class-validator';
import { Type } from 'class-transformer';
import { AddressDto } from './address.dto';
export class CreateUserDto {
@IsString()
name: string;
@IsString()
email: string;
// Nested object validation setup
@ValidateNested() // Trigger validation for the nested object
@Type(() => AddressDto) // Transform plain object to AddressDto instance
address: AddressDto;
} Step 2: Configure the ValidationPipe#
In main.ts, enable the ValidationPipe with transform: true to ensure class-transformer (and @Type) runs:
// src/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);
// Configure ValidationPipe to enable transformation
app.useGlobalPipes(
new ValidationPipe({
transform: true, // Critical: transforms plain objects to class instances
whitelist: true, // Optional: removes properties not defined in DTOs
forbidNonWhitelisted: true, // Optional: throw error if extra properties are sent
}),
);
await app.listen(3000);
}
bootstrap(); Step 3: Create a Controller Endpoint#
Add a controller to handle requests using CreateUserDto:
// src/users/users.controller.ts
import { Controller, Post, Body } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
@Controller('users')
export class UsersController {
@Post()
create(@Body() createUserDto: CreateUserDto) {
return { message: 'User created', data: createUserDto };
}
} Step 4: Test the Validation#
Send a POST request to http://localhost:3000/users with invalid nested data (e.g., missing address.street or a string zipCode).
Example Invalid Payload:
{
"name": "John Doe",
"email": "[email protected]",
"address": {
"city": "New York",
"zipCode": "invalid-zip" // ❌ Should be a number
}
} Expected Validation Error:
NestJS will return a 400 Bad Request with details about the nested validation failure:
{
"statusCode": 400,
"message": [
"address.street must be a string", // Missing street
"address.zipCode must be a number conforming to the specified constraints" // Invalid zipCode
],
"error": "Bad Request"
} Advanced Scenarios#
Let’s tackle more complex nested structures, such as arrays of nested objects and optional nested properties.
Scenario 1: Validating Arrays of Nested Objects#
To validate an array of nested objects (e.g., a user with multiple addresses), use @IsArray(), @ValidateNested({ each: true }), and @Type.
// src/users/dto/create-user.dto.ts
import { IsArray, ValidateNested } from 'class-validator';
import { Type } from 'class-transformer';
import { AddressDto } from './address.dto';
export class CreateUserDto {
// ... other properties
@IsArray() // Ensure the value is an array
@ValidateNested({ each: true }) // Validate EVERY element in the array
@Type(() => AddressDto) // Transform EVERY element to AddressDto
addresses: AddressDto[]; // Array of nested objects
} Scenario 2: Optional Nested Objects#
If a nested object is optional (e.g., address?: AddressDto), use @IsOptional() to allow null or undefined, but still validate when the object is provided.
import { IsOptional, ValidateNested } from 'class-validator';
import { Type } from 'class-transformer';
export class CreateUserDto {
// ... other properties
@IsOptional() // Allows the address to be undefined
@ValidateNested() // Validate only if address is provided
@Type(() => AddressDto)
address?: AddressDto;
} Troubleshooting Tips#
If validation still isn’t working, try these debugging steps:
-
Check if the Nested Object is a Class Instance:
Log the nested object in your controller to verify it’s an instance of the DTO class:@Post() create(@Body() createUserDto: CreateUserDto) { console.log(createUserDto.address instanceof AddressDto); // Should be true return createUserDto; } -
Verify
ValidationPipeConfiguration:
Ensuretransform: trueis set. Without it,class-transformerwon’t run, and@Typehas no effect. -
Check for Circular Dependencies:
If using@Typewith circularly referenced classes (e.g.,UserDto→AddressDto→UserDto), useforwardReffrom@nestjs/common:import { forwardRef } from '@nestjs/common'; @Type(() => forwardRef(() => AddressDto)) address: AddressDto; -
Validate Nested DTOs in Isolation:
Test the nested DTO directly withclass-validatorto ensure its validation rules work:import { validate } from 'class-validator'; async function testAddressDto() { const address = new AddressDto(); address.street = 123; // Invalid (should be string) const errors = await validate(address); console.log(errors); // Should show "street must be a string" } testAddressDto();
Conclusion#
Validating nested objects in NestJS requires a combination of class-validator (for validation rules) and class-transformer’s @Type (for transforming plain objects to class instances). By avoiding common pitfalls like missing @Type imports, incorrect type specifications, or misconfigured ValidationPipe, you can ensure robust validation for even the most complex data structures.
Remember:
- Use
@Type(() => NestedDto)to transform nested objects. - Pair
@Typewith@ValidateNested(and@IsArray()for arrays). - Always enable
transform: trueinValidationPipe.
With these steps, you’ll master nested object validation and build more reliable NestJS APIs.