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 = false
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
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