30

I want to validate body payload using class-validator in a nest.js controller. My currency.dto.ts file is like this:

import {
  IsNotEmpty,
  IsString,
  ValidateNested,
  IsNumber,
  IsDefined,
} from 'class-validator';

class Data {

  @IsNotEmpty()
  @IsString()
  type: string;

  @IsNotEmpty()
  @IsNumber()
  id: number;
}

export class CurrencyDTO {
  @ValidateNested({ each: true })
  @IsDefined()
  data: Data[];
}

and in my nest.js controller, I use it like this.

  @Post()
  @UseGuards(new AuthTokenGuard())
  @UsePipes(new ValidationPipe())
  addNewCurrency(@Req() req, @Body() data: CurrencyDTO) {
    console.log('data', data);
  }

my validation pipe class is like this:

import {
  PipeTransform,
  Injectable,
  ArgumentMetadata,
  BadRequestException,
  HttpException,
  HttpStatus,
} from '@nestjs/common';
import { validate, IsInstance } from 'class-validator';
import { plainToClass, Exclude } from 'class-transformer';

@Injectable()
export class ValidationPipe implements PipeTransform<any> {
  async transform(value: any, metadata: ArgumentMetadata) {
    if (value instanceof Object && this.isEmpty(value)) {
      throw new HttpException(
        `Validation failed: No Body provided`,
        HttpStatus.BAD_REQUEST,
      );
    }
    const { metatype } = metadata;
    if (!metatype || !this.toValidate(metatype)) {
      return value;
    }
    const object = plainToClass(metatype, value);
    const errorsList = await validate(object);
    if (errorsList.length > 0) {
      const errors = [];
      for (const error of errorsList) {
        const errorsObject = error.constraints;
        const { isNotEmpty } = errorsObject;
        if (isNotEmpty) {
          const parameter = isNotEmpty.split(' ')[0];
          errors.push({
            title: `The ${parameter} parameter is required.`,
            parameter: `${parameter}`,
          });
        }
      }
      if (errors.length > 0) {
        throw new HttpException({ errors }, HttpStatus.BAD_REQUEST);
      }
    }
    return value;
  }

  private toValidate(metatype): boolean {
    const types = [String, Boolean, Number, Array, Object];
    return !types.find(type => metatype === type);
  }
  private isEmpty(value: any) {
    if (Object.keys(value).length > 0) {
      return false;
    }
    return true;
  }
}

This validation pipe works fine for all except for nested objects. Any idea what am I doing wrong here? My body payload is like this:

{
"data": [{
    "id": 1,
    "type": "a"
}]
}
Yogesh Umesh Vaity
  • 41,009
  • 21
  • 145
  • 105
Usama Tahir
  • 1,707
  • 3
  • 15
  • 30

2 Answers2

87

Try specifying the nested type with @Type:

import { Type } from 'class-transformer';

export class CurrencyDTO {
  @ValidateNested({ each: true })
  @Type(() => Data)
  data: Data[];
}

For a nested type to be validated, it needs to be an instance of a class not just a plain data object. With the @Type decorator you tell class-transformer to instantiate a class for the given property when plainToClass is called in your VaildationPipe.

If you are using the built-in ValidationPipe make sure you have set the option transform: true.

Kim Kern
  • 54,283
  • 17
  • 197
  • 195
  • 3
    For any reason this solution isn't working for me. I did what you said and it didn't work. – Leonardo Emilio Dominguez Dec 14 '18 at 20:09
  • 1
    I have tried it and it works for me. :-/ Without more specific information, I can't help you. Maybe open a new question and add your code?! I'll have a look at it. – Kim Kern Dec 14 '18 at 20:12
  • 1
    I just opened a new post, I will appreciate it if you can give it a look. – Leonardo Emilio Dominguez Dec 14 '18 at 20:18
  • 2
    I would add. That class-validator does not require this \@Type annotation. It's just how nestjs works. It always uses class-transformer on your DTO and class-transformer requires that \@Type annotation. – mauron85 Mar 22 '19 at 07:48
  • 2
    @LeonardoEmilioDominguez try to put both the classes in the same file and make sure nested class appears first(should be at the top), order matters. As you can see Data appears before DataDTO in the question. – RollerCosta May 06 '20 at 21:06
  • I'm using NestJS, the key parts for me were the `@ValidateNested` and `@Type` decorators, which are both shown in this answer. Thank you! (Note: using a global `ValidationPipe({transform: true}` pipe didn't work, I had to use `@Type`.) – josephdpurcell Dec 10 '21 at 20:49
  • How i can put multiple type of data like Data | number. eg if i receive object it should be data other wise it should be number – Ameer Hamza Apr 18 '22 at 15:19
  • Thanks, this worked for me. Since I'm using `whitelist: true` on validator pipe. – José Neto Jan 24 '23 at 23:15
6

At least in my case, the accepted answer needed some more info. As is, the validation will not run if the key data does not exist on the request. To get full validation try:

@IsDefined()
@IsNotEmptyObject()
@ValidateNested()
@Type(() => CreateOrganizationDto)
@ApiProperty()
organization: CreateOrganizationDto;
Xen_mar
  • 8,330
  • 11
  • 51
  • 74