class-validator icon indicating copy to clipboard operation
class-validator copied to clipboard

fix: Class-validator combined to class-transformer

Open Sukaato opened this issue 2 years ago • 3 comments

Description

When we combine class-validator and class-transform an error occurs when we try to validate a property (numberString in this case) and then transform it into a number, this happens no matter what order the decorators are.

Minimal code-snippet showcasing the problem

import { IsNotEmpty, IsNumberString, Min } from 'class-validator';
import { Transform } from 'class-transformer';

export class ExampleDto {
  @IsNotEmpty()
  @IsNumberString()
  @Transform(p => +p.value)
  @Min(1)
  index: number;
}

Expected behavior

Validate that it is a number string, and then transform it into a number

Actual behavior

Actually in NestJS it produces an HTTP 400 error because the property is recognized as a number and not number string.

Sukaato avatar Feb 21 '22 19:02 Sukaato

Anyone update on this having the same issue, I just want to validate the input before transformation.

adamghowiba avatar Jun 29 '22 00:06 adamghowiba

I'm facing the same scenario and found this:

https://stackoverflow.com/questions/67920067/multiple-validation-pipes

https://stackoverflow.com/questions/69084933/nestjs-dto-class-set-class-validator-and-class-transformer-execution-order

Apparently that's desired and a workaround would be creating a separate pipe for transforming your dto.

Unless there's a better option, in that case I'm all ears.

martynakrysinska avatar Jul 06 '22 09:07 martynakrysinska

any updates on this?

fr1sk avatar Sep 14 '22 11:09 fr1sk

i'm facing the same issue, any update?

Kaydayo avatar Sep 25 '22 09:09 Kaydayo

+1

karukenert avatar Oct 05 '22 09:10 karukenert

I think this issue belongs in class-transformer. Nevertheless, here is a solution to your problem, I think.

If I understand correctly, since class-validator requires a class instance to run validation, you want to have the option to use class-transformer to create a class instance without applying your transformation, then run validate. After validation, you want to do your actual field transformation.

This is possible to achieve using TransformInstanceToInstance

Note: the docs on class-validator are outdated. It uses the deprecated plainToClass and TransformClassToClass method and decorator names.

import { IsNotEmpty, IsNumberString, Min } from 'class-validator';
import { TransformInstanceToInstance } from 'class-transformer';

export class ExampleDto {
  @IsNotEmpty()
  @IsNumberString()
  @TransformInstanceToInstance(p => +p.value)
  @Min(1)
  index: number | string;
}

I'm not sure if there is an obvious way to get around having a union type for the field for this specific example.

The sequence of operations for your use case would then look like this:

import { validate } from 'class-validator';
import { plainToInstance, instanceToInstance } from 'class-transformer';

const plainObject = { index: '2' }

// transform to an instance. Any `Transform` or `TransformPlainToInstance` decorators are applied
const initialInstance = plainToInstance(Customer, plainObject);

// run validation on our class instance
const errors = validate(initialInstance);

// perform an instance to instance transformation, applying `TransformInstanceToInstance` decorators
const transformedInstance = instanceToInstance(initialInstance);

If you don't want a class instance as your output, you can use TransformInstanceToPlain and instanceToPlain instead to achieve the same transformation, but end up with a plain object.

PS: I'm not on stackoverflow, so if anyone wants to share this solution to the relevant threads there, that would be great :)

braaar avatar Oct 06 '22 04:10 braaar

@braaar

TransformInstanceToInstance

@TransformInstanceToInstance doesn't work for me - it says for methods only (function / getter /setter) but not for properties. It's MethodDecorator not PropertyDecorator

kosiakMD avatar Dec 07 '22 11:12 kosiakMD

What you ask is not possible in one go. However, you can use groups to your advantage and do the transformation and validation in two phases. An example:

import { IsNotEmpty, IsNumberString, Min, validate, validateSync } from 'class-validator';
import { plainToInstance, Transform, Type } from 'class-transformer';

export class ExampleDto {
  @IsNotEmpty({ groups: ['first'] })
  @IsNumberString({ groups: ['first'] })
  @Transform(p => +p.value, { groups: ['typecast'] })
  @Min(1, { groups: ['typecast'] })
  index: number;
}

// First we just transform the object without changing the property type
const instanceOne = plainToInstance(ExampleDto, { index: '12' }, { groups: ['first'] });
const validationResultOne = validateSync(instanceOne, { groups: ['first'] })

console.log({ instanceOne, validationResultOne });
// { instanceOne: { index: "12", <prototype>: ExampleDto }, validationResultOne: [] }

// Then we transform the object again with type casting
const instanceTwo = plainToInstance(ExampleDto, instanceOne, { groups: ['typecast'] });
const validationResultTwo = validateSync(instanceTwo, { groups: ['typecast'] })

console.log({ instanceTwo, validationResultTwo });
// { instanceTwo: { index: 12, <prototype>: ExampleDto }, validationResultTwo: [] }

This has the disadvantage of running the transformation and validation twice for the two same payload.

NoNameProvided avatar Dec 09 '22 18:12 NoNameProvided

This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.

github-actions[bot] avatar Jan 10 '23 01:01 github-actions[bot]