Matheus Nascimento
Matheus Nascimento

Reputation: 1

How to return a clean message for nested validation error on NestJS

I have a nested form validation on my nestjs application, that returns the following result when a field has some error:

Current error message: "products.0.amount A quantidade do produto é obrigatória"

Expected error message: "A quantidade do produto é obrigatória"

For those examples above, I have a form with an attribute called product that is an array of objects

The issue is that I don't want to return the property name appended to the message, I want to return a clean message, readable for end users

export class CheckoutForm {
  @ValidateNested({ always: true })
  @Type(() => ProductForm)
  products: ProductForm[];
}
export class ProductForm {
  @IsString({ always: true, message: 'O nome do produto é obrigatório' })
  name: string;

  @Min(1, { always: true, message: 'O preço do produto é obrigatório' })
  value: number;

  @IsNotEmpty({
    always: true,
    message: 'A quantidade do produto é obrigatória',
  })
  amount: number;
}

Upvotes: 0

Views: 10843

Answers (4)

maphe
maphe

Reputation: 1931

If you want to keep the hierarchy of your form mirrored in your error messages (so that maybe your frontend has a better time assigning error messages to their appropriate fields), you can set up your exception factory as follow:

  app.useGlobalPipes(
    new ValidationPipe({
      transform: true,
      exceptionFactory: (validationErrors: ValidationError[] = []) => {
        const formatError = (error: ValidationError) => {
          if (error.children?.length) {
            return {
              field: error.property,
              errors: error.children.map(formatError),
            }
          }
          return {
            field: error.property,
            errors: Object.values(error.constraints ?? {}),
          }
        }

        return new BadRequestException(
          validationErrors.map((error) => formatError(error)),
        )
      },
    }),
  )

This way you'd get error messages that look something like:

{
    "message": [
        {
            "field": "products",
            "errors": [
                {
                    "field": "0",
                    "errors": [
                        {
                            "field": "amount",
                            "errors": [
                                "A quantidade do produto é obrigatória"
                            ]
                        }
                    ]
                }
            ]
        }
    ],
    "error": "Bad Request",
    "statusCode": 400
}

Upvotes: 0

Aleksey Lukyanov
Aleksey Lukyanov

Reputation: 41

In addition to Luis Monsalve answer, you can make your code a bit cleaner and more versatile. I just added a helper function that returns an array of all errors.

Defualt validation error from the NestJS ValidationPipe looks like this:

{
  "statusCode": 422,
  "message": [
    "name must be longer than or equal to 3 characters",
    "name must contain only letters (a-zA-Z)"
  ],
  "error": "Unprocessable Entity"
}

So we can add the helper function getCustomValidationError to save this format. And we can use getAllConstraints helper function to get all error messages from validators without prefixes like this "products.0.amount".

function getAllConstraints(errors: ValidationError[]): string[] {
  const constraints: string[] = [];

  for (const error of errors) {
    if (error.constraints) {
      const constraintValues = Object.values(error.constraints);
      constraints.push(...constraintValues);
    }

    if (error.children) {
      const childConstraints = getAllConstraints(error.children);
      constraints.push(...childConstraints);
    }
  }

  return constraints;
}
function getCustomValidationError (message: string | string[]) {
  return {
    statusCode: 422,
    message,
    error: 'Unprocessable Entity',
  }
};

And add useGlobalPipes in main.ts file:

app.useGlobalPipes(new ValidationPipe({
  exceptionFactory: (errors: ValidationError[]) => new HttpException(getCustomValidationError(getAllConstraints(errors)), HttpStatus.BAD_REQUEST),
}));

If you are not using a global validation pipe you can do it locally in a controller method, use @UsePipe decorator. Note that this will not work if there is a global pipe, as it will be executed after the controller method pipe.

@Post()
@UsePipes(new ValidationPipe(exceptionFactory...))
yourControllerMethod() {}

Upvotes: 4

Luis Monsalve
Luis Monsalve

Reputation: 160

Since 5.5.0 nestjs have a method to modify the errors on class-validator and class-transformer.

in your main.ts you should have

  await app.useGlobalPipes(new ValidationPipe());

this method receives a parameter named "exceptionFactory", which receives a function that can return an exception. if you want to modify and specific error you can do something like this

   await app.useGlobalPipes(new ValidationPipe({
    exceptionFactory: (errors: ValidationError[]) =>
    {
      const customError = errors.map((el) =>
      {
        if(el.constraints.isString)
        {
          return "O nome do produto é obrigatório"
        }

        if(el.constraints.isNotEmpty)
        {
          return "A quantidade do produto é obrigatória"
        }

      });
      
      return new HttpException(customError, HttpStatus.BAD_REQUEST);
    }
  }));

notices I have an array of errors this is because I use graphql maybe in your case in REST is different however if you console.log this errors you get an array whit this values

[
  ValidationError {
    target: SignUpInput {
      confirmPassword: '123',
      email: '[email protected]',
      name: '',
      password: '123',
      surname: ' '
    },
    value: '',
    property: 'name',
    children: [],
    constraints: {
      minLength: 'name must be longer than or equal to 3 characters',
      isAlpha: 'name must contain only letters (a-zA-Z)'
    }
  }
]

in the constraints elements are the errors of the validations do you have whit this you can modify the errors.

IMPORTANT: "exceptionFactory" should return a valid exeption otherwise it wont work

Upvotes: 1

moogs
moogs

Reputation: 91

Looks like this is an open issue with class-validator. https://github.com/typestack/class-validator/issues/614#issuecomment-807255434

Seems to be nested validation issues if more than one level, which would explain the issues that you are having.

Upvotes: 0

Related Questions