28

Consider this endpoint in my API:

@Post('/convert')
  @UseInterceptors(FileInterceptor('image'))
  convert(
    @UploadedFile() image: any,
    @Body(
      new ValidationPipe({
        validationError: {
          target: false,
        },
        // this is set to true so the validator will return a class-based payload
        transform: true,
        // this is set because the validator needs a tranformed payload into a class-based
        // object, otherwise nothing will be validated
        transformOptions: { enableImplicitConversion: true },
      }),
    )
    parameters: Parameters,
  ) {
    return this.converterService.start(image, parameters);
  }

The body of the request, which is set to parameters argument, contains a property called laserMode that should be a boolean type, it is validated like such on the parameters DTO:

  @IsDefined()
  @IsBoolean()
  public laserMode: boolean;

now the strange part, when a send a request from PostMan where:

  1. laserMode = false
  2. laserMode = cool (a string other the boolean value)

I noticed that laserMode is always set to true and this is after the validation process is completed because when I console.log the instance of Parameter in the constructor of the class

export class Parameters {
  ...
  constructor() {
    console.log('this :', this);
  }
  ...
}

I don't see the property!

Note: when laserMode is removed from the request, the expected validation errors are returned (should be defined, should be boolean value).

// the logged instance 'this' in the constructor
this : Parameters {
  toolDiameter: 1,
  sensitivity: 0.95,
  scaleAxes: 200,
  deepStep: -1,
  whiteZ: 0,
  blackZ: -2,
  safeZ: 2,
  workFeedRate: 3000,
  idleFeedRate: 1200,
  laserPowerOn: 'M04',
  laserPowerOff: 'M05',
  invest: Invest { x: false, y: true }
}
// the logged laserMode value in the endpoint handler in the controller
parameters.laserMode in controller : true
// the logged laser value from the service
parameters.laserMode in service : true
  • misspelling is checked
  • same result is noticed when using a Vue app instead of postman. So!!?
joe_inz
  • 998
  • 2
  • 13
  • 30

9 Answers9

43

Found a workaround for the issue with class-transformer

You can use this:

@IsBoolean()
@Transform(({ value} ) => value === 'true')
public laserMode: boolean;

This will transform the string into a boolean value, based on if it is 'true' or any other string. A simple workaround, but every string different than true, results in false.

17

This is how I got round the issue while managing to keep the boolean typing.

By referring to the original object by key instead of using the destructured value.

import { Transform } from 'class-transformer';

const ToBoolean = () => {
  const toPlain = Transform(
    ({ value }) => {
      return value;
    },
    {
      toPlainOnly: true,
    }
  );
  const toClass = (target: any, key: string) => {
    return Transform(
      ({ obj }) => {
        return valueToBoolean(obj[key]);
      },
      {
        toClassOnly: true,
      }
    )(target, key);
  };
  return function (target: any, key: string) {
    toPlain(target, key);
    toClass(target, key);
  };
};

const valueToBoolean = (value: any) => {
  if (value === null || value === undefined) {
    return undefined;
  }
  if (typeof value === 'boolean') {
    return value;
  }
  if (['true', 'on', 'yes', '1'].includes(value.toLowerCase())) {
    return true;
  }
  if (['false', 'off', 'no', '0'].includes(value.toLowerCase())) {
    return false;
  }
  return undefined;
};

export { ToBoolean };
export class SomeClass {
  @ToBoolean()
  isSomething : boolean;
}
David Kerr
  • 1,058
  • 1
  • 8
  • 12
  • 1
    not sure which version of `Transform` you were using, I had to change it slightly to: const toPlain = Transform( (value) => { return value; }, { toPlainOnly: true, } ); const toClass = (target: any, key: string) => { return Transform( (value) => { return valueToBoolean(value); }, { toClassOnly: true, } )(target, key); }; return function (target: any, key: string) { toPlain(target, key); toClass(target, key); }; }; – FoosMaster Aug 05 '21 at 22:57
  • What version are you folks using? I'm on v0.4.0. Have you tried FoosMaster's suggestion? The documentation for your versions Transform function should let you know what you have access to. In my case I do indeed have access to the original object and the property within the object which is why it works. You can refer to the value with obj[key] – David Kerr Aug 31 '21 at 22:15
  • https://github.com/typestack/class-transformer#advanced-usage – David Kerr Aug 31 '21 at 22:34
16

This is due to the option enableImplicitConversion. Apparently, all string values are interpreted as true, even the string 'false'.

There is an issue requesting a changed behavior for class-transformer.

Kim Kern
  • 54,283
  • 17
  • 197
  • 195
  • ok, but without it, the validation always fails for all rules of DTO properties'. should I convert to a string instead of boolean? – joe_inz Nov 26 '19 at 09:08
  • You can control the conversion by using the `@Transform` annotation instead, see e.g. https://stackoverflow.com/a/55480830/4694994 – Kim Kern Nov 26 '19 at 10:15
  • thank for the answer but I converted the property to a string instead of boolean and saved my self from a lot of headaches. – joe_inz Nov 26 '19 at 11:23
7

did you find a permanent solution for this?
I solved it with this hack:

@IsBoolean()
@Transform(({ obj, key }) => obj[key] === 'true')
laserMode: boolean;

An improved version in case you want to have use of the @IsBoolean is to do the following:

  @Transform(({obj, key}) => {
    return obj[key] === 'true' ? true : obj[key] === 'false' ? false : obj[key];
  })
  @IsBoolean()
  laserMode?: boolean = true;

This will return the value if for example someone send laserMode='anything'

  • Unfortunately no i didn't (at the time that i asked this question), but you can check the answer of David Kerr. – joe_inz Jun 07 '22 at 10:31
3

If you want to receive both true/false,
then use the below solution.
It will mark all true for defined values
and mark it as false for all others

@Transform(({ value }) => {  
  return [true, 'enabled', 'true', 1, '1'].indexOf(value) > -1;  
})  
mode: boolean;  

Avoid using below decorators as they don't work well enough

@IsBoolean()  
@Type(() => Boolean)  
Realms AI
  • 101
  • 4
1

We resolved this issue like this. Sometimes Transform runs twice.

@Transform(({ obj, key }) => {
    const value = obj[key];
    if (typeof value === 'string') {
      return obj[key] === 'true';
    }

    return value;
  })
laserMode: boolean;
MuratFe
  • 11
  • 3
1

this issue is related to class-transformer, not Nestjs itself.

do these steps:

first enable transform in ValidationPipe for the app:

app.useGlobalPipes(new ValidationPipe({ transform: true })); 

then in dto use customer transform like this: (you can ignore validation and swagger decorators)

import { ApiProperty } from '@nestjs/swagger';
import { IsBoolean, IsOptional, IsNumber } from 'class-validator';
import { Transform } from 'class-transformer';

export class UploadMediaOptionsDto {
  @ApiProperty()
  @IsOptional()
  @IsBoolean()
  @Transform((val: string) => [true, 'enabled', 'true', 1, '1'].indexOf(val) > -1)
  public generateThumbnail: boolean = true;
}

and disable enableImplicitConversion.

it should transform the types based on the decorators, i.e. @IsInt() should attempt to transform a string to an integer.

pixparker
  • 2,903
  • 26
  • 23
0

The simplest solution I found is:

  @IsBoolean()
  @Type(() => Boolean)
  laserMode: boolean;;
s4eed
  • 7,173
  • 9
  • 67
  • 104
0

I got the same kind of the problem when parsing a boolean query parameter. And I found the problem caused by class-validate itself. It alway transform the value to true.

With the @Transform decoratror, I printed the transformed value and raw value of the this DTO item. You can see that the transformed value is always true no matter the raw value is 'true' or 'false'

...
  @IsBoolean()
  @IsOptional()
  @Transform((object) => {
    console.log(object);
    return object.value;
  })
  isMuted?: boolean;
...

enter image description here

Obviously, the @Transform provides the access to the raw query parameters, which means you can use the obj item to get the boolean parameter to complete transformation correctly, in my case it can done like this

{
...
  @IsBoolean()
  @IsOptional()
  @Transform(({ obj }) => {
    return obj.isMuted === 'true';
  })
  isMuted?: boolean;
...
}
lang sun
  • 21
  • 4