Fix - Boolean casting

Before implementing this feature, we need to fix a faulty behavior.

We'll use a boolean as query parameter to indicate whether we want a hard ou soft deletion. But before doing this, we need to fix something first.

Remember that option we have set in the validation pipe options, enableImplicitConversion? It was responsible for automatically converting path/query parameters to their corresponding primitive types. The id, for example, would arrive from the network as a string ("1") instead of a number (1). So, this option automatically took care of this for us by checking the variable type.

However, with booleans, it does not work as expected. The reason is that it converts any non-empty string into true, and an empty string into false. If we decorate a variable in a DTO with the @IsBoolean() decorator, it will always be valid, due to this reason. Obviously this should not be the case. The string "true"/"false" should be converted to the boolean true/false, and anything else should be rejected.

What we'll do to fix this is to create the decorator @ToBoolean(), which will make the correct boolean casting. But we'll not use it directly. We'll also create a new @IsBoolean() decorator, which will be a combination of the default one with this new transformation decorator. As transformations always occur before validations, we can then guarantee that the boolean will be correctly transformed, and then correctly validated.

Let's then begin by creating the file common -> decorators -> to-boolean.decorator. Here, first we'll create a function to make the correct boolean casting.

const toBoolean = (value: unknown) => {
  switch (value) {
    case null:
      return 'Failure';

    case 'true':
      return true;
    case 'false':
      return false;

    default:
      return value;
  }
};

Some things to consider

  • Here, the null value is forbidden because there is not much sense in a null boolean.

  • You can create two new folders inside common -> decorators if you want: validators and transformers. This may ensure a better organization.

The next step is to create a decorator that will transform a value using that function. We still haven't seen the @Transform() decorator yet, but it allows to transform a value. It is always executed before validation decorators. A very basic example would be to transform the value into a number, like this

@Transform(({ value }) => +value)

But in our case, we need to access the object before it is automatically transformed. To achieve this, we can use the obj and key fields. obj refers to the object before this transformation, and key is the name of the field receiving the decorator. So we can define our decorator as follows

const ToBoolean = () => Transform(({ obj, key }) => toBoolean(obj[key]));

The solution to this situation was inspired by this answer from Stack Overflow.

Lastly, create the file is-boolean.decorator. As it will have the same name of the original decorator, we'll need to give it an alias to avoid a conflict. So, put at the top of the file

import { IsBoolean as DefaultIsBoolean } from 'class-validator';

And now, we can create our own @IsBoolean() decorator with correct transformation.

/**
 * Checks if the value is a boolean. Works with query params.
 */
export const IsBoolean = (
  validationOptions?: ValidationOptions,
): PropertyDecorator =>
  applyDecorators(DefaultIsBoolean(validationOptions), ToBoolean());

Commit - Correctly validating booleans

Last updated