Validating RequestParams and PathVariables in Spring MVC

Sat, Dec 5, 2015

Spring MVC provides a convenient way to validate inputs to API end points through the use of JSR-303 annotations. While this mechanism works great for end points that consume a RequestBody (as is the case with most POST and PUT requests), it is not easy to achieve the same effect for end points that consume primitives in the form of path variables or request parameters (as is the case with most GET requests).

Let’s take a look at how to validate RequestBody inputs using JSR-303.

    @RequestMapping(method = RequestMethod.POST,
            consumes = MediaType.APPLICATION_JSON_VALUE,
            produces = MediaType.APPLICATION_JSON_VALUE
    )
    @ResponseStatus(HttpStatus.OK)
    public Map register(@RequestBody @Valid EmailRequest emailRequest) {
        return registrationService.register(emailRequest);
    }

The value object EmailRequest is annotated with the appropriate constraint annotations.

public class EmailRequest {
    @Email
    public String email;

    private EmailRequest() {
    }

    public String getEmail() {
        return email;
    }
}

If we were to attempt a similar approach for a GET end point that accepts a RequestParam, the validation would simply not happen.

    @RequestMapping(method = RequestMethod.GET,
            consumes = MediaType.APPLICATION_JSON_VALUE,
            produces = MediaType.APPLICATION_JSON_VALUE
    )
    @ResponseStatus(HttpStatus.OK)
    public Map search(@Email
                   @Valid
                   @RequestParam("email")
                   String email) {
        return emailMessage(email);
    }

This is where Spring’s @Validated annotation is useful. With @Validated, we can validate request parameters and path variables.

@RestController
@Validated
public class RegistrationController {
    @RequestMapping(method = RequestMethod.GET,
            consumes = MediaType.APPLICATION_JSON_VALUE,
            produces = MediaType.APPLICATION_JSON_VALUE
    )
    @ResponseStatus(HttpStatus.OK)
    public Map search(@Email
                   @RequestParam("email")
                   String email) {
        return emailMessage(email);
    }
}

An important thing to note here is that using @Valid annotation results in MethodArgumentNotValidException being thrown when validation fails, but @Validated results in ConstraintViolationException being thrown. Since these exceptions have different ways of abstracting the error messages associated with validation, it is important to have different error handlers for both of these. An example pattern using ExceptionHandler will be as follows:

@ControllerAdvice
@Component
public class GlobalExceptionHandler {
    @ExceptionHandler
    @ResponseBody
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public Map handle(MethodArgumentNotValidException exception) {
        return error(exception.getBindingResult().getFieldErrors()
                .stream()
                .map(FieldError::getDefaultMessage)
                .collect(Collectors.toList()));
    }


    @ExceptionHandler
    @ResponseBody
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public Map handle(ConstraintViolationException exception) {
        return error(exception.getConstraintViolations()
                .stream()
                .map(ConstraintViolation::getMessage)
                .collect(Collectors.toList()));
    }

    private Map error(Object message) {
        return Collections.singletonMap("error", message);
    }
}