Over the years I have seen several approaches for validating inputs (@RequestParam, @RequestBody, etc.) for REST endpoints. Many of these approaches are pretty manual and require explicitly calling a validation mechanism. But is there a way to automate this process while writing as little code as possible, still validating all inputs, and responding with friendly messages to consumers? 

Getting Started

In this Snippet, I will present a minimalistic approach for handling this issue by using javax.validation.constraints combined with Spring MVC @RestController, @ControllerAdvice, and @ExceptionHandler.

To follow our examples, you will need:

  1. Java 7/8 SDK
  2. Project with SpringBoot configured application (org.springframework.boot dependency)
REST Controllers

Starting with the controllers, we need to tell Spring MVC that we want to check for constraint validators at the method calls. In order to do so, simply annotate your controller with @Validated from the org.springframework.validation package:

@Validated
@RestController
@RequestMapping("/example")
public class ExampleController {
...
} 

Now let's take a look at an example payload (getters and setters omitted for better presentation), whose fields we want to receive and validate as a request body for an endpoint:

public class Payload {
    @NotBlank
    private String id;
    
    @NotNull
    private PayloadType type;
@Min(0) private Integer amount; }

To enable the validation mechanism on the request body, the ExampleController method using the Payload should have the @Valid annotation, as pictured below:

    @PostMapping
    public ResponseEntity<?> update(@RequestBody @Valid Payload payload) {
    ...
    }

When we call this endpoint with an invalid @RequestBody, we should get a MethodArgumentNotValidException, which contains all the information we need  to construct a friendly message to the consumer. 

But what if we want to validate and retrieve the parameters from a GET request without the Payload or any other DTO to encapsulate the request parameters? The validation framework has us covered here as well. You can still validate the method arguments with the same annotations used in the Payload's fields:

    @GetMapping(params = {"id", "type", "amount"})
    public ResponseEntity<?> find(
            @RequestParam(name = "id") @NotBlank String id,
            @RequestParam(name = "type") @NotNull PayloadType type,
            @RequestParam(name = "amount") @Min(0) Integer amount) {
        ...
    }

In this case, if it receives some invalid parameters, we should get a ConstraintValidationException that also contains all the information we need  to construct a friendly message to the consumer. 

Exception Handler

In order to give the API the expected contract rules,  with the HTTP status and message that we want to expose for consumers, we should declare a ControllerAdvice bean that contains the methods annotated with @ExceptionHandler, which will handle the exceptions properly:

@ControllerAdvice(assignableTypes = {ExampleController.class})
public class ExampleControllerAdvice {
    @ExceptionHandler({ConstraintValidationException.class})
    public ResponseEntity<?> invalidParamsExceptionHandler(ConstraintValidationException e){
        String message = resolveConstraintViolations(e.getConstraintViolations());
        ErrorResponse errorResponse = ErrorResponse.builder()
                .message(message)
                .build();
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse);
    }
    
    @ExceptionHandler({MethodArgumentNotValidException.class})
    public ResponseEntity<?> invalidParamsExceptionHandler(MethodArgumentNotValidException e){
        String message = resolveBindingResultErrors(e.getBindingResult());
        ErrorResponse errorResponse = ErrorResponse.builder()
                .message(message)
                .build();
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse);
    }
}

Next, you can create your message resolver and handle each violation with all the details provided by the exceptions themselves. Here is a simple example which just joins the default messages for each parameter/field while also using their names to compose a friendly message:

    private String resolveBindingResultErrors(BindingResult bindingResult) {
        return bindingResult.getFieldErrors().stream()
                .map(fr -> {
                    String field = fr.getField();
                    String validationMessage = fr.getDefaultMessage();
                    return format("'%s': %s", field, validationMessage);
                }).
                .collect(joining(", "));
    }

    private String resolveConstraintViolations(Collection<ConstraintViolation<?>> constraintViolations) {
        return constraintViolations.stream()
                .map(cv -> {
                    String parameter = getParameterName(cv);
                    String validationMessage = cv.getMessage();
                    return format("'%s': %s", parameter, validationMessage);
                })
                .collect(joining(", "));
     }
     
     private String getParameterName(ConstraintViolation constraintViolation) {
        return StreamSupport.stream(constraintViolation.getPropertyPath().spliterator(), false)
                .filter(p -> elementKind.equals(ElementKind.PARAMETER))
                .findFirst()
                .map(Path.Node::getName)
                .orElse(null);
    } 
Custom Validations

With this mechanism in place, we can now think about creating custom annotations for when we need to validate specific business scenarios. In this example, we're creating a constraint to validate a phone number using a regex validator. In order to do this, we need to create an @interface which defines our new constraint: 

@Documented
@Constraint(validatedBy = PhoneNumberValidator.class)
@Target( { ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
public @interface PhoneNumber {
    String message() default "Invalid phone number";
    Class[] groups() default {};
    Class[] payload() default {};
}

Then, we implement our PhoneNumberValidator as requested, overriding the isValid method from the ConstraintValidator class:

public class PhoneNumberValidator implements ConstraintValidator<PhoneNumber, String> {
    @Override
    public void initialize(PhoneNumber phoneNumber) {
    }
 
    @Override
    public boolean isValid(String phoneField, ConstraintValidatorContext cxt) {
        return phoneField != null && phoneField.matches("[0-9]+")
          && (phoneField.length() > 8) && (phoneField.length() < 14);
    }
}

Using this is quite simple, as with the other constraint annotations:

@PhoneNumber String phone

If you need to validate some complex logic using multiple fields inside a Payload, you can still use @Assert annotations for private validation methods with custom messages. Here is an example:

public class Payload {
    @NotBlank
    private String id;
    
    @NotNull
    private PayloadType type;
    
    private Integer amount;

    @AssertTrue(message = "Payload of type 'ADD_AMOUNT' must have amount > 0")
    private boolean isRequiredAmountForPayloadType() {
        return !PayloadType.ADD_AMOUNT.equals(this.type) || Optional.ofNullable(this.amount).orElse(0) > 0;
    }
}
Another Common Approach

You may have seen another approach for validating parameters inside a RequestBody that involves adding a BindingResult parameter to the controller method to avoid the MethodArgumentNotValidException and to allow you to handle the error through a manual check:

    @PostMapping
    public ResponseEntity<?> update(@RequestBody @Valid Payload payload, BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            String message = resolveBindingResultErrors(bindingResult);
            ...
        }
        ...
    }

Although this works well for requests that have a parameter annotated with @RequestBody, you should be aware that it doesn't work for RequestParam-annotated parameters. These latter parameters are mostly used on GET requests, like the one we used as an example in this Snippet, which have the annotations directly on the method arguments.

Conclusion

As we can see, this is a very simple approach that saves time by avoiding the usual manual or restrict validation mechanisms, as well as some resulting boilerplate code. Do you have additional thoughts? Do you use some other validation mechanism in your projects? Let me know in the comments! 


Author

Fábio Suarez

Fábio Suarez is a Java Engineer at Avenue Code who works with microservices applications.


JSR 354: The Java Money API

READ MORE

Spring WebFlux: A Quick Start

READ MORE

Use Java EE JSON to Avoid Mapping Complex DTOs

READ MORE