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:
laserMode = falselaserMode = 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
laserModeis 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
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;
}
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.
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.
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)
did you find a permanent solution for this?
I solved it with this hack:
@IsBoolean()
@Transform(({ obj, key }) => obj[key] === 'true')
laserMode: boolean;
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With