Search code examples
javaspringspring-mvcspring-restcontroller

How do I get the details of multiple missing request parameters from a Validated Spring RestController?


I'm attempting to use Spring's validation of two @RequestParams, with an @ControllerAdvice catching the exceptions thrown by the framework when a parameter is missing, and returning a 400 error with the missing parameter.

So, my code looks like:

@RestController
@Validated
public class FooController {
  @RequestMapping(value = "/foo", method = RequestMethod.GET)
  @ResponseBody
  public Foo getFoo(@RequestParam LocalDate dateFrom, @RequestParam LocalDate dateTo) {
    // Do stuff
  }
}

@ControllerAdvice
public class ExceptionController {
  @ExceptionHandler(value = {MissingServletRequestParameterException.class})
  @ResponseStatus(value = HttpStatus.BAD_REQUEST)
  @ResponseBody
  public ErrorResponse handleMissingParameterException(MissingServletRequestParameterException ex) {
    return new ErrorResponse(ex.getMessage());
  }
}

This works perfectly if I miss a single parameter - I get a nice JSON response that looks like:

{
  "reason": "Required LocalDate parameter 'dateFrom' is not present"
}

with a 400 status.

However if I miss both parameters, I get the same error response as above - i.e. it's only reporting the first missing parameter, where I'd prefer it if I can list all of them.

Looking at the method of the exception, it seems like it only intends to handle a single parameter - it has methods getParameterName() and getParameterType() in the singular.

Is there any way I can get Spring to report all validation errors in a single exception to improve the experience for the client?


Solution

  • You can set @RequestParam(required = false) and then use @NotNull.

    If a parameter is null then a ConstraintViolationException will be thrown. To handle it just declare an @ExceptionHandler method. Check the code snippet below

    @RestController
    @Validated
    public class FooController {
      @RequestMapping(value = "/foo", method = RequestMethod.GET)
      @ResponseBody
      public Foo getFoo(
          @RequestParam (required = false) 
          @NotNull(message = "Required LocalDate parameter dateFrom is not present") 
          LocalDate dateFrom,
          @RequestParam (required = false) 
          @NotNull(message = "Required LocalDate parameter dateTo is not present") 
          LocalDate dateTo) 
      ) {
        // Do stuff
      }
    }
    
    @ControllerAdvice
    public class ExceptionController {
        @ExceptionHandler({ConstraintViolationException.class})
        public ResponseEntity<List<ErrorResponseDTO>> exceptionHandler(ConstraintViolationException e) {
            List<ErrorResponseDTO> errors = new ArrayList<>(e.getConstraintViolations().size());
            e.getConstraintViolations().forEach(violation -> errors.add(new ErrorResponseDTO(violation.getMessage())));
            return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST);
        }
    }
    

    Outcome will be like this:

    [
        {
            "reason": "Required LocalDate parameter dateFrom is not present"
        },
        {
            "reason": "Required LocalDate parameter dateTo is not present"
        }
    ]