NestJS DTO class set class-validator and class-transformer execution order
Asked Answered
Y

3

9

Is there a way to set the execution order of decorators when describing a DTO class in NestJS using class-validator and class-transformer packages ?

Following code fails when the value of foo is set to null with the error:

Expected a string but received a null

@IsOptional()
@IsString()
@IsByteLength(1, 2048)
@Transform(({ value }) => validator.trim(value))
@Transform(({ value }) => validator.stripLow(value))
foo: string;

Even though I have a isString decorator that should check that indeed a string was passed and must already fail to not pass the execution to the @Transform decorators, but it didn't fail.

Yakka answered 7/9, 2021 at 8:39 Comment(0)
I
15

Class-Validator works based on classes. Request payloads that come into the server are just plain JSON objects to start off with. To change this behaviour, Nest has to first call plainToClass from class-validator if you're using its ValidationPipe. Because of this, the @Transform() decorators take precedence over the other class-validator decorators, and are acted upon first. You could get around this by using multiple pipes, or possibly providing default values for the @Transform() decorator, but what is happening is the intended effect.

Infernal answered 7/9, 2021 at 15:43 Comment(0)
E
7

Control @Transfrom and Validator Decorator execution order by 2 ValidationPipe

main.ts

import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true,
    }),

    new ValidationPipe({
      transform: true,
      transformOptions: { groups: ['transform'] },
    }),
  );

  await app.listen(4000);
}
bootstrap();

create-user.dto.ts

export class CreateUserDto {
  @ApiProperty({
    description: 'username',
    minLength: 4,
    maxLength: 64,
  })
  @Length(4, 64)
  name: string;

  @ApiProperty({
    description: 'password',
    minLength: 4,
    maxLength: 64,
  })
  @Length(6, 64)
  @Transform(({ value }: { value: string }) => hashSync(value), {
    groups: ['transform'],
  })
  password: string;
}

This will run @Length first, and then @Transform.

Translation with Deepl

Emelinaemeline answered 16/1, 2023 at 10:16 Comment(0)
S
0

I resolve the issue using this approach

if in transform code throw the issue, I handle it and return undefined. In validation fn I can check if it undefined and return error message

   class GetGeneratorDto {  
    @IsStringDOM({
        message: 'Не был переданблок',
    })
    @Transform(({ value }) => {
        try {
            return cheerio.load(value);
        } catch {}
    })
    blockContent: cheerio.CheerioAPI;
}
import { registerDecorator, ValidationOptions } from 'class-validator';
import * as cheerio from 'cheerio';

export function IsStringDOM(validationOptions?: ValidationOptions) {
    return function (object: any, propertyName: string) {
        registerDecorator({
            name: 'isStringDOM',
            target: object.constructor,
            propertyName: propertyName,
            options: validationOptions,
            validator: {
                validate(value: cheerio.CheerioAPI) {
                    try {
                        const $ = value;

                        const recordType = Number(
                            $('[data-record-type]').attr('data-record-type'),
                        );

                        if (recordType !== 396) {
                            return false;
                        }

                        return true;
                    } catch (error) {
                        return false;
                    }
                },
            },
        });
    };
}
Suzettesuzi answered 13/3 at 22:38 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.