How to validate child DTO conditionally with class-validator?

Viewed 475

I am trying to conditionally validate nested DTO with class validator, but @ValidateIf seems to be not applying correctly and always validating the nested class.

I have the following DTO:

export class SuperAdminStoreDto extends StoreDto {

    @IsOptional()
    @IsBoolean()
    readonly payments: boolean;

    @ValidateIf(object => object.payments)
    @ValidateNested({ each: true })
    @Type(() => PaymentDetails)
    readonly payment_details: PaymentDetails[];
}

class PaymentDetails {

    @IsNumberString()
    @IsNotEmpty()
    readonly min: string;

    @IsNumberString()
    @IsNotEmpty()
    readonly max: string;

    @IsNumberString()
    @IsNotEmpty()
    readonly percentage: string;
}

Let's say the object being validated is:

{
    "payments": false,
    "payment_details": {
        "min": "1"
    }
}

I get no errors although I should because I have set the validator with following flags:

{
  whitelist: true,
  forbidNonWhitelisted: true,
  forbidUnknownValues: true,
  validationError: {
   target: false,
  }
}

I am expecting the validator to not allow the child object to be present and throw an error.

1 Answers

One option I've used is to create a custom decorator to validate the nested object since custom decorators work with @ValidateIf()

type Class<T> = { new (...args: any[]): T };
        
export function ValidateDtoArray(dto: Class<any>, validationOptions?: ValidationOptions) {
  return function (object: unknown, propertyName: string) {
    registerDecorator({
      name: 'validateDto',
      target: (object as any).constructor,
      propertyName: propertyName,
      options: validationOptions,
      validator: {
        async validate(value: unknown, args: ValidationArguments) {
          if (!isArray(value)) return false;
          for (let i = 0; i < (value as any[]).length; i++) {
            const array_value = (value as any[])[i];
            if(typeof array_value !== 'object') return false;
            const validation_errors = await validate(plainToClass(dto, array_value as Record<string, unknown>);
            if (validation_errors.length > 0) return false;
          }
          return true;
        },
        defaultMessage(validationArguments?: ValidationArguments) {
          return `Can only be of type ${dto.name}[]`;
        },
      },
    });
  };
}

Your code then becomes

export class SuperAdminStoreDto extends StoreDto {

    @IsOptional()
    @IsBoolean()
    readonly payments: boolean;

    @ValidateIf(object => object.payments)
    @ValidateDtoArray(PaymentDetails)
    @Type(() => PaymentDetails)
    readonly payment_details: PaymentDetails[];
}
Related