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#

  1. Prerequisites
  2. Understanding Nested Object Validation in NestJS
  3. The Role of class-transformer and @Type
  4. Common Issues with the @Type Decorator
  5. Step-by-Step Guide to Fixing @Type Issues
  6. Advanced Scenarios: Arrays, Optional Objects, and More
  7. Troubleshooting Tips
  8. Conclusion
  9. 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-name if 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:

  1. 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;  
    }  
  2. Verify ValidationPipe Configuration:
    Ensure transform: true is set. Without it, class-transformer won’t run, and @Type has no effect.

  3. Check for Circular Dependencies:
    If using @Type with circularly referenced classes (e.g., UserDtoAddressDtoUserDto), use forwardRef from @nestjs/common:

    import { forwardRef } from '@nestjs/common';  
     
    @Type(() => forwardRef(() => AddressDto))  
    address: AddressDto;  
  4. Validate Nested DTOs in Isolation:
    Test the nested DTO directly with class-validator to 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 @Type with @ValidateNested (and @IsArray() for arrays).
  • Always enable transform: true in ValidationPipe.

With these steps, you’ll master nested object validation and build more reliable NestJS APIs.

References#