DEV Community

Cover image for Cross-Parameter Validation with Spring
Ryan Taylor for Focused Labs

Posted on • Updated on • Originally published at focusedlabs.io

Cross-Parameter Validation with Spring

With Spring, 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 for a working example of the article's concepts. For a full technical guide for creating cross-parameter validations, refer to this JBoss guide.

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:

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;
}
Enter fullscreen mode Exit fullscreen mode

Here we are using Lombok's @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:

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;
}
Enter fullscreen mode Exit fullscreen mode

With this simple validation in place, flower delivery orders with an empty recipientName will result in a thrown ConstraintViolationException 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:

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 {};
}
Enter fullscreen mode Exit fullscreen mode

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.

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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:

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;
}
Enter fullscreen mode Exit fullscreen mode

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.

Cover photo is my own: Flickr

Discussion (2)

Collapse
jamesmcmahon profile image
James McMahon

Great technique that opens the door for validation reuse! Thanks for posting Ryan.

Collapse
austinbv profile image
Austin Vance

How would you test a cross parameter validation?