Blog

Cross-Parameter Validation with Spring

11.03.2020 | Backend | Ryan Taylor

hero

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.

icon

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;
}
icon

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;
}
icon

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.

icon

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 {};
}
icon

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);
    }
}
icon

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;
}

Parting Thoughts

icon

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

Share

Read More

Related Posts

related_image

06.30.2021 | Culture | Katy Scott

At Focused Labs, collaboration is key to how we work together; it helps our teams learn from each other, brings us closer and helps us become more efficient...

related_image

06.23.2021 | Culture | Austyn

Late-night feedings and diaper changes, the 3-4 month sleep regression, teething, and a growth spurt all mean I'm getting less sleep than...

related_image

05.12.2021 | Culture Backend Frontend | Ryan Taylor

Temporarily disrupts "normal" business operations and allow self-organized teams to rapid prototype around their interest areas

related_image

04.27.2021 | Culture | Erin Hochstatter

Several years ago, I'd been trying to find an approach to software consulting that made sense for me [...]

related_image

01.28.2021 | Backend | Parker Drake

Recently I found myself needing to validate fields in a Spring Boot controller written in Kotlin...

related_image

01.22.2021 | Tutorial | Luke Mueller

⌘+⇧+g is the way to go

related_image

01.21.2021 | Devops | Katy G

Kube jobs running wild? To delete successful jobs...

additional accent
accent
FocusedLabs

171 N Aberdeen St
Suite 400
Chicago, IL 60607

[email protected]

© 2021 FocusedLabs, All Rights Reserved.

  • facebook icon
  • twitter icon
  • linkedin icon
  • github icon