Cross-Parameter Validation with Spring
Tags: java, tutorial, oop, webdev
Published: 2020-11-03 06:18:00


With [Spring](https://spring.io/), data validation is a breeze in many common use cases (like validating a method's input parameters) - and is highly recommended for creating robust applications. 

Often, developers are called upon to produce software with more complex validation requirements. This guide describes one such situation and proposes a solution that is effective and simple to maintain. 

>  Visit [this GitHub repo](https://github.com/crtaylor243/spring-cross-parameter-validation) for a working example of the article's concepts. For a full technical guide for creating cross-parameter validations, refer to this [JBoss guide]( https://docs.jboss.org/hibernate/validator/4.1/reference/en-US/html/validator-customconstraints.html ).

## Business Case

Suppose you are developing an e-commerce application where users can request for a bouquet of flowers to be delivered to an address within a specific window of time. An order POJO might take the shape of:

```java
package io.focusedlabs.crossparametervalidation;

import java.time.ZonedDateTime;
import lombok.Builder;
import lombok.Data;

@Builder
@Data
public class FlowerDeliveryOrder {
   private String orderId;
   private String recipientName;
   private ZonedDateTime orderPlaced;  
   private ZonedDateTime deliveryStart;
   private ZonedDateTime deliveryEnd;
}
```

> Here we are using [Lombok's](https://projectlombok.org/) `@Data` and `@Builder` annotations to auto-generate boilerplate code, saving keystrokes and valuable developer time.

## Simple Validations

The Bean Validation API can be used to enforce some simple and common validations, such as guaranteeing a `FlowerDeliveryOrder` will have a non-null `orderId`:

```java
package io.focusedlabs.crossparametervalidation;

import java.time.ZonedDateTime;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.NotEmpty;
import lombok.Builder;
import lombok.Data;

@Builder
@Data
public class FlowerDeliveryOrder {
   @NotNull
   private String orderId;

   @NotEmpty(message = "Recipient Name must be specified")
   private String recipientName;
   private ZonedDateTime orderPlaced;  
   private ZonedDateTime deliveryStart;
   private ZonedDateTime deliveryEnd;
}
```

> With this simple validation in place, flower delivery orders with an empty `recipientName` will result in a thrown [ConstraintViolationException](https://docs.oracle.com/javaee/7/api/javax/validation/ConstraintViolationException.html) that can be handled gracefully by the application.

## Cross-Parameter Validations

The simple validations above are useful for our application, but what if we want to validate business logic that depends on multiple fields? For example, if the user of the web form selects a `deliveryStart` that is after the `deliveryEnd`, the order should not be accepted. 

It might be tempting to implement an `isValid()` method inside the `FlowerDeliveryOrder` and manually check the result wherever the orders are processed. This may be appropriate sometimes, but the developers must remember to include the check everywhere the order is processed.

> Instead, we can introduce a custom validation that inspects both parameters and integrates seamlessly with Spring's bean validation mechanism.

## Operating Principle

There are two parts to this solution: 1) a custom annotation to mark the class for validation and 2) a custom validator that encapsulates the business logic to be executed.

### Custom Annotation

An annotation is a form of interface and are declared in a similar fashion. Below, we create a custom annotation that can adorn our `FlowerDeliveryOrder` class:

```java
package io.focusedlabs.crossparametervalidation;

import java.lang.annotation.ElementType;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = OrderDeliveryWindowValidator.class)
public @interface ValidDeliveryWindow {
   String message() default "Delivery Window Start Time must precede End Time";
   Class<?>[] groups() default {};
   Class<? extends Payload>[] payload() default {};
}
```

> In this configuration, the annotation can be placed on classes with `@Target({ElementType.TYPE})` and is available to the application at runtime with `@Retention(RuntimePolicy.RUNTIME)`.

The Bean Validation API requires `message` and `groups` at a minimum; we can add a `payload` field to attach custom data to the constraint.  

### Custom Validator

The `@Constraint` annotation specifies a class that should be used by the Bean Validation API to perform the custom logic. This class must implement the `ConstraintValidator` interface.

```java
package io.focusedlabs.crossparametervalidation;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class OrderDeliveryWindowValidator implements ConstraintValidator<ValidDeliveryWindow, FlowerDeliveryOrder> {

    public void initialize(ValidDeliveryWindow constraintAnnotation) {
    }

    public boolean isValid(FlowerDeliveryOrder order, ConstraintValidatorContext constraintContext) {
       return order.deliveryStart.isBefore(order.deliveryEnd);
    }
}
```

> Notice that the `isValid()` method is very similar to how a developer might write an inline validation. This is great news, because it means that existing validations be easily refactored to use this pattern.

### Finishing Up

With the building blocks in place, we can now apply this custom validation to our `FlowerDeliveryOrder`:

```java
package io.focusedlabs.crossparametervalidation;

import java.time.ZonedDateTime;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.NotEmpty;
import lombok.Builder;
import lombok.Data;

@Builder
@Data
@ValidDeliveryWindow
public class FlowerDeliveryOrder {
   @NotNull
   private String orderId;

   @NotEmpty(message = "Recipient Name must be specified")
   private String recipientName;
   private ZonedDateTime orderPlaced;  
   private ZonedDateTime deliveryStart;
   private ZonedDateTime deliveryEnd;
}
```

## Parting Thoughts

> While there is some initial time investment required to create a custom annotation and validator, change management becomes much easier from that point forward - and the code can be understood by developers at most skill levels. It is a trusted tool at [Focused Labs](https://focusedlabs.io/).

*Cover photo is my own: [Flickr](https://www.flickr.com/photos/rasputin243/49308688351)*