Validate Nested Objects Using Class Validator and Nestjs

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.

3

3 Answers

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;
}
1

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[];
0

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.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Chloe Bennett

Chloe Bennett

Culture, Media & Entertainment Columnist

Chloe Bennett explores the intersection of pop culture, streaming entertainment, digital trends, and contemporary lifestyle. Her weekly commentary reaches thousands of culture enthusiasts.

Share this article
Twitter Facebook Pinterest