Constraint Validation in Spring Boot Microservices
Tags: java, microservices, programming, spring
Published: 2020-11-21 00:28:06

In a microservice architecture, services may accept several, if not many, of the same inputs. This pattern can easily lead to code duplication and redundancy between services. In an effort to mitigate these drawbacks and keep service code focused, we can devise a robust solution involving several APIs provided by Spring and Java.

*The following tutorial will assume some working knowledge of Java and Spring Boot, but will cater to a range of skill levels. Regardless, it never hurts to see another developer's code!*

## Background

Our solution will involve combining the Java and Spring Boot APIs, `ConstraintValidator` and `ResponseEntityExceptionHandler`, respectively.

[Hibernate Validator](http://hibernate.org/validator/), enhanced as part of [JSR 380](https://jcp.org/en/jsr/detail?id=380), is a specification of the Java API for standard [Bean validation](https://beanvalidation.org/).  In the context of Spring Boot applications, you may have used this without thinking twice.  Examples include:

- `@NotNull`
- `@Min`
- `@Max`
- `@Pattern`
- `@Past`
- `@Email`
- `@PositiveOrZero`

In this tutorial, we will examine how to go beyond these basic validations using the `ConstraintValidator` interface to define our own set of constraints.

While other forms of exception handling exist within the Spring ecosystem, the `ResponseEntityExceptionHandler` provides global (and centralized) exception handling within a service.  This globalization is key to the efficacy of our custom constraint annotation since it allows us to validate multiple Beans (or fields within them). That said, we will investigate how we can leverage this class to gracefully handle violations to our constraints.

---

## Implementation

Let's dive in.  To avoid bloating the tutorial with boilerplate code, you will only find necessary blocks of code in the sections below.  This article is coupled with a working example on [GitHub](https://github.com/Verley93/annotation-validation).

*Note: I will be making references to this example project throughout the tutorial.*

### Dependencies

The list is short and sweet:

```groovy
implementation 'org.springframework.boot:spring-boot-starter-validation'
```

### Creating the Annotations

We'll start simple.  Let's say we've implemented a *Fridge* and *Pantry Service* of which allows us to:

- Manage the Fridge and Pantry repositories
- Accept POST and/or PUT requests with a JSON payload

We want to validate common fields between request models of both services.  Our request model may look something like this:

```java
public class FoodRequestModel {

    private String name;

    @PositiveOrZero(message = "Quantity must be positive")
    @Max(value = 25, message = "Quantity must not exceed 25")
    private int quantity;
    
    private String category

    private boolean refrigerated;

}
```

One of the simplest constraints we can build will involve composing existing constraints, such as `@PositiveOrZero` and `@Max` in the example above.  This allows us to put an explicit label on common constraints and call it "business logic".  Below, we define `@FoodQuantity`:

```java
@Documented
@PositiveOrZero(message = "Quantity must be positive")
@Max(value = 25, message = "Quantity must not exceed 25")
@Constraint(validatedBy = {})
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.PARAMETER})
public @interface FoodQuantity {

    String message() default "Invalid quantity";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

}
```

There's a lot going on here, so let's break this down:

- `@Constraint` marks an annotation as being a Bean Validation constraint and allows us to specify `ConstraintValidator` implementations; zero, one, or many implementations are welcome here
- `@Retention` is set such that our annotation will be retained at runtime
- `@Target` is set such that we can validate different types of inputs to our services
- `message`, `groups`, and `payload` are required by `@Constraint` but do not have to be set--these provide specificity beyond what we'll cover today

By no means is this a simplification.  Looking past the verbosity, the annotation opens quite a few doors to make handling complexity a breeze as we'll see in the next example.

---

Let's define a constraint for the *category* field such that:

- Category must be passed and cannot be empty
- Only certain categories are allowed to be passed
- Categories may differ between the *Fridge* and *Pantry* services

To implement this annotation, we will expand upon the premise of our first annotation by adding a custom parameter and providing an implementation of the `ConstraintValidator` interface.  The result looks something like this:

```java
@Documented
@NotNull(message = "Category must be present")
@NotEmpty(message = "Category must not be empty")
@Constraint(validatedBy = FoodCategoryValidator.class)
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.PARAMETER})
public @interface FoodCategory {

    String message() default "Invalid category";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    String[] allowed() default {"dairy", "grain"};

}
```

A few more things are happening in this annotation compared to `@FoodQuantity`.  We've specified a new parameter, `allowed`, to restrict what may be passed into `category`.  Notice the default value--this array is only referenced if values are not passed into `@FoodCategory`.  To handle this constraint, we've implemented `FoodCategoryValidator`:

```java
@Slf4j
public class FoodCategoryValidator
        implements ConstraintValidator<FoodCategory, String> {

    List<String> allowed;

    @Override
    public void initialize(FoodCategory constraintAnnotation) {
        this.allowed = Arrays.asList(constraintAnnotation.allowed());
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        log.info("isValid: value=[{}]", value);
        if (!allowed.contains(value.toLowerCase())) {
            String err = "Category must be one of the following: " + allowed;
            context.disableDefaultConstraintViolation();
            context.buildConstraintViolationWithTemplate(err)
                    .addConstraintViolation();
            return false;
        }
        return true;
    }

}
```

Let's breakdown our new validator class:

- `ConstraintValidator` is parameterized with the annotation class and the type being validated--a `String` containing the value of *category*
- A global field `allowed`, set within the overridden `initialize` method
  - It is within this method that we gain access to the parameters of `@FoodCategory` for use throughout the validator class
- `isValid` is the meat and bones of our constraint validation
  - For invalid scenarios, we disable the default constraint violation, build a proper error message, and return false--this eventually throws an exception we'll be interested in later

Lastly, to get the most out of our annotation, we'll propagate the category field into two subclasses pertaining to each service.

After all our hard work, we've reached a clean set of request models ready to be bombarded with invalid values:

```java
public class FoodRequestModel {

    private String name;

    @FoodQuantity
    private int quantity;

    private boolean refrigerated;

}

public class FridgeRequestModel extends FoodRequestModel {

    @FoodCategory(allowed = {"dairy", "vegetables", "beer"})
    private String category;

}

public class PantryRequestModel extends FoodRequestModel {

    @FoodCategory(allowed = {"grains", "canned", "snacks"})
    private String category;

}
```

### Handling Validation Errors

Until now, we've only defined constraints we (and our consumers) must follow.  Let's open up an endpoint to allow food to be added to the fridge and test our constraints:

```java
@Slf4j
@Validated
@RestController
@RequestMapping(path = "/api/v1/fridge")
@RequiredArgsConstructor(onConstructor_ = @Autowired)
public class FridgeController {

    private final FridgeService fridgeService;

    @PostMapping("/food")
    public ResponseEntity<FoodResponseModel> addFoodToFridgeV1(
            @Valid @RequestBody FridgeRequestModel request) {

        log.info("addFoodToFridgeV1: request=[{}]", request);
        FoodResponseModel response = fridgeService.addFoodToFridge(request);
        return ResponseEntity.ok(response);
    }

}
```

There are a few critical aspects to note for our annotations to work properly:

- `@Validated` must be used either at the *class* or *method* level to indicate where validation needs to take place
- `@Valid` is used to mark a *property* for validation cascading, which triggers our constraints

Let's send a payload:

```json
{
    "name": "milk",
    "category": "dairy",
    "quantity": 2,
    "refrigerated": true
}
```

Success!  But let's see what happens when we send another payload we know will result in error:

```json
{
    "name": "pinto beans",
    "category": "legumes",
    "quantity": -3,
    "refrigerated": true
}
```

Note we are violating multiple constraints for this request model.  You should see a verbose response containing the details of the errors encountered and the exceptions thrown.  This verbosity isn't ideal for us (or our consumers) to deal with, so let's filter out important details with a simple override of the `ResponseEntityExceptionHandler`.

A further look into the error response provided by Spring, you may notice the exception thrown: `MethodArgumentNotValidException`.  This is the exception we will be interested in for handling constraint violations within the exception handler.

First, we need a model to capture relevant information.  We're able to distill the following from the original Spring response:

```java
public class ErrorResponseModel {

    private final String errorMessage;
    private final LocalDateTime timestamp;
    private final String path;

}
```

Next, we'll construct both the global exception handler and a method to handle our constraints:

```java
@Slf4j
@RestControllerAdvice
public class ErrorController extends ResponseEntityExceptionHandler {

    @Override
    protected final ResponseEntity<Object> handleMethodArgumentNotValid(
            MethodArgumentNotValidException ex,
            HttpHeaders headers,
            HttpStatus status,
            WebRequest request) {

        List<String> errorMessages = ex.getBindingResult().getAllErrors().stream()
                .map(DefaultMessageSourceResolvable::getDefaultMessage)
                .collect(Collectors.toList());
        log.error("handleMethodArgumentNotValid: errors=[{}]", errorMessages);
        return new ResponseEntity<>(
                ErrorResponseModel.builder()
                        .errorMessage(errorMessages.get(0))
                        .timestamp(LocalDateTime.now())
                        .path(request.getDescription(false))
                        .build(),
                HttpStatus.BAD_REQUEST
        );
    }

}
```

Let's note a few things here:

- `@RestControllerAdvice` is just that--a specialized component for classes that declare `@ExceptionHandler` methods shared across multiple controller classes
- We override the `ResponseEntityExceptionHandler`'s `handleMethodArgumentNotValid` method so we may
  - Log important information
  - Build a small, focused error response
  - Return an HTTP status code of our choice, based on the constraint violated

Upon sending a payload that violates the constraints we've defined, you should see a succinct response indicating where we went wrong:


```json
{
    "errorMessage": "Quantity must be positive",
    "timestamp": "2020-11-20T19:18:31.1899697",
    "path": "uri=/api/v1/pantry/food"
}
```

---

## Challenge

This tutorial provides some basic forms of constraint validation within a Spring-/Java-based REST service.  If you are looking to take things a bit further, here are a few places you can start:

- Explore what might differ for violating constraints of `@PathVariable` or `@RequestParam`
- Implement nested constraints within a complex request model
- Increase the flexibility of the HTTP status code returned to the consumer
- Expand the sample project to handle the nuance of a composite service--a *Picnic Service*, for instance
- Explore the `@Constraint` API further--what might `payload` and `groups` be used for?

---

## Closing

This concludes the tutorial on implementing custom constraint validators using annotations!  Don't be afraid to let me know if I missed anything.  I certainly welcome (and appreciate) criticism, questions, and the like.

For further reference, here is the GitHub repository with the full working code and examples presented in this article:

{% github Verley93/annotation-validation %}