63

I'm trying to validate nested objects using class-validator and NestJS. I've already tried following this thread by using the @Type decorator from class-transform and didn't have any luck. This what I have:

DTO:

class PositionDto {
  @IsNumber()
  cost: number;

  @IsNumber()
  quantity: number;
}

export class FreeAgentsCreateEventDto {

  @IsNumber()
  eventId: number;

  @IsEnum(FinderGamesSkillLevel)
  skillLevel: FinderGamesSkillLevel;

  @ValidateNested({ each: true })
  @Type(() => PositionDto)
  positions: PositionDto[];

}

I'm also using built-in nestjs validation pipe, this is my bootstrap:

async function bootstrap() {
  const app = await NestFactory.create(ServerModule);
  app.useGlobalPipes(new ValidationPipe());
  await app.listen(config.PORT);
}
bootstrap();

It's working fine for other properties, the array of objects is the only one not working.

BinaryButterfly
  • 18,137
  • 13
  • 50
  • 91
  • 1
    I've just put your code in an empty sample project and it seems to work for me. What specific value is "not working"? What are your expectations? If you for example put `"positions": [{"other": true}]` in your body it rejects with 400. `positions: []` is a valid value though. – Kim Kern Dec 14 '18 at 20:35
  • I'm expecting that if you try `positions: [1]`, it throws an error – Leonardo Emilio Dominguez Dec 14 '18 at 20:37
  • `@ArrayNotEmpty()`? – Vallie Apr 15 '21 at 08:49

3 Answers3

82

for me, I would able to validate nested object with 'class-transformer'

import { Type } from 'class-transformer';

full example:

import {
  MinLength,
  MaxLength,
  IsNotEmpty,
  ValidateNested,
  IsDefined,
  IsNotEmptyObject,
  IsObject,
  IsString,
} from 'class-validator';
import { Type } from 'class-transformer';

class MultiLanguageDTO {
  @IsString()
  @IsNotEmpty()
  @MinLength(4)
  @MaxLength(40)
  en: string;

  @IsString()
  @IsNotEmpty()
  @MinLength(4)
  @MaxLength(40)
  ar: string;
}

export class VideoDTO {
  @IsDefined()
  @IsNotEmptyObject()
  @IsObject()
  @ValidateNested()
  @Type(() => MultiLanguageDTO)
  name!: MultiLanguageDTO;
}
tarek noaman
  • 1,253
  • 1
  • 11
  • 18
39

You are expecting positions: [1] to throw a 400 but instead it is accepted.

According to this Github issue, this seems to be a bug in class-validator. If you pass in a primitive type (boolean, string, number,...) or an array instead of an object, it will accept the input as valid although it shouldn't.


I don't see any standard workaround besides creating a custom validation decorator:

import { registerDecorator, ValidationOptions, ValidationArguments } from 'class-validator';

export function IsNonPrimitiveArray(validationOptions?: ValidationOptions) {
  return (object: any, propertyName: string) => {
    registerDecorator({
      name: 'IsNonPrimitiveArray',
      target: object.constructor,
      propertyName,
      constraints: [],
      options: validationOptions,
      validator: {
        validate(value: any, args: ValidationArguments) {
          return Array.isArray(value) && value.reduce((a, b) => a && typeof b === 'object' && !Array.isArray(b), true);
        },
      },
    });
  };
}

and then use it in your dto class:

@ValidateNested({ each: true })
@IsNonPrimitiveArray()
@Type(() => PositionDto)
positions: PositionDto[];
Kim Kern
  • 54,283
  • 17
  • 197
  • 195
1

I faced the same issue, so I created my own ValidateNested decorator.

import {
  ValidationOptions,
  registerDecorator,
  ValidationArguments,
  validateSync,
} from 'class-validator';
import { plainToClass } from 'class-transformer';

/**
 * @decorator
 * @description A custom decorator to validate a validation-schema within a validation schema upload N levels
 * @param schema The validation Class
 */
export function ValidateNested(
  schema: new () => any,
  validationOptions?: ValidationOptions
) {
  return function (object: Object, propertyName: string) {
    registerDecorator({
      name: 'ValidateNested',
      target: object.constructor,
      propertyName: propertyName,
      constraints: [],
      options: validationOptions,
      validator: {
        validate(value: any, args: ValidationArguments) {
          args.value;
          if (Array.isArray(value)) {
            for (let i = 0; i < (<Array<any>>value).length; i++) {
              if (validateSync(plainToClass(schema, value[i])).length) {
                return false;
              }
            }
            return true;
          } else
            return validateSync(plainToClass(schema, value)).length
              ? false
              : true;
        },
        defaultMessage(args) {
          if (Array.isArray(args.value)) {
            for (let i = 0; i < (<Array<any>>args.value).length; i++) {
              return (
                `${args.property}::index${i} -> ` +
                validateSync(plainToClass(schema, args.value[i]))
                  .map((e) => e.constraints)
                  .reduce((acc, next) => acc.concat(Object.values(next)), [])
              ).toString();
            }
          } else
            return (
              `${args.property}: ` +
              validateSync(plainToClass(schema, args.value))
                .map((e) => e.constraints)
                .reduce((acc, next) => acc.concat(Object.values(next)), [])
            ).toString();
        },
      },
    });
  };
}

Then you can use it like -

class Schema2 {

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

  @IsNotEmpty()
  @IsString()
  prop2: string;
}


class Schema1 {
  @IsNotEmpty()
  @IsString()
  prop3: string;

  @ValidateNested(Schema2)
  nested_prop: Schema2;
}

Works for both non-primitive arrays and javascript objects.

  • I don't the `@ValidateNested(Schema2)` is still valid with the current version of class-validator `v0.14.0` – Ahmed Mar 31 '23 at 20:33