Fluent Validation with Chaining Methods in Java: Improving Code Readability and Flexibility

Animesh Chaturvedi
Walmart Global Tech Blog
5 min readMay 3, 2023
Photo Credit:Urupong

Introduction

In software development, data validation is a critical aspect to ensure the integrity and reliability of the system. Validating user inputs, API requests, or any other data that flows through the system is essential to prevent errors, data breaches, and other unexpected behaviours. One common approach to implement data validation is to use a Validator interface that defines a contract for validating different types of data.

In Java, implementing validation logic can sometimes become complex and hard to manage, especially when dealing with multiple validation rules. However, using chaining methods in the Validator interface can greatly simplify the process, making the code more readable, reusable, and maintainable.

In this blog post, we will explore how to add chaining methods to the Validator interface and leverage them to create robust and flexible validation logic. We will also provide a code example to demonstrate the concept.

Why Chaining Methods?

The traditional approach of using a Validator interface often involves calling multiple validation methods one after another, which can result in nested if-else statements and make the code hard to read and maintain. By adding chaining methods to the Validator interface, we can leverage a fluent interface that allows for a more concise and expressive way of writing validation logic.

Creating the Validator Interface

Let’s start by creating the Validator interface, which will serve as the foundation for our chaining methods. The Validator interface will have a generic type T, representing the type of data to be validated, and will define a single method validate that takes an object of type T and returns a ValidationResult object, indicating whether the validation was successful or not.

@FunctionalInterface
public interface Validator<T> {
ValidationResult validate(T t);
}

The ValidationResult class represents the result of a validation operation and consists of two fields: a boolean isValid to indicate whether the validation was successful or not, and a String message to store an error message if the validation fails.

public class ValidationResult {
private final boolean isValid;
private final String message;

public ValidationResult(boolean isValid, String message) {
this.isValid = isValid;
this.message = message;
}

public boolean isValid() {
return isValid;
}

public String getMessage() {
return message;
}
}

Adding Chaining Methods to the Validator Interface

Next, we will add three chaining methods to the Validator interface: and, or, and negate.

The and method takes another Validator as a parameter and returns a new Validator that performs both validations. If either validation fails, the new Validator returns the failed validation result.

default Validator<T> and(Validator<? super T> other) {
return obj -> {
ValidationResult result = this.validate(obj);
return !result.isValid() ? result : other.validate(obj);
};
}

The or method takes another Validator as a parameter and returns a new Validator that performs either validation. If one of the validations succeeds, the new Validator returns the successful validation result.

default Validator<T> or(Validator<? super T> other) {
return obj -> {
ValidationResult result = this.validate(obj);
return result.isValid() ? result : other.validate(obj);
};
}

The negate method returns a new Validator that performs the opposite validation of the original Validator. If the original Validator returns a failed validation result, the new Validator returns a successful validation result, and vice versa.

default Validator<T> negate() {
return obj -> {
ValidationResult result = this.validate(obj);
return new ValidationResult(!result.isValid(), result.getMessage());
};
}

Example Usage of Chaining Methods in Validation Logic

Let’s now see how we can use the chaining methods to create a combined Validator that validates both email and name inputs. We will use two simple Validators for email and name validation as examples.

Validator<String> emailValidator = str -> {
if (str == null || !str.matches("[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}")) {
return new ValidationResult(false, "Email is not valid");
}
return new ValidationResult(true, "");
};

Validator<String> nameValidator = str -> {
if (str == null || str.trim().isEmpty()) {
return new ValidationResult(false, "Name cannot be empty");
}
return new ValidationResult(true, "");
};

Validator<String> combinedValidator = emailValidator.and(nameValidator);

ValidationResult result = combinedValidator.validate("john.doe@example.com");

In the above code, we have created two Validators — emailValidator and nameValidator — for email and name validation respectively. Then, we have used the and chaining method to combine them into a single combinedValidator that performs both email and name validation. The combinedValidator will only return a successful validation result if both email and name validations pass.

Finally, we have called the validate method on the combinedValidator with an example email input “john.doe@example.com”. The validate method returns a ValidationResult object, indicating whether the validation was successful or not. In this example, since the email input is valid and the name input is not empty, the isValid field of the result object will be true, indicating a successful validation, and the getMessage method will return an empty string as the validation message.

You can further customize and chain multiple Validators together to create more complex validation logic as per your requirements. Chaining methods in the Validator interface provide a flexible and expressive way to build robust and maintainable validation logic in Java.

Advantages of Chaining Methods

The approach of adding chaining methods to the Validator interface offers several advantages:

Encapsulation: The validation logic is encapsulated in the Validator interface and its implementations. This promotes better separation of concerns and makes it easier to change the validation logic without affecting the rest of the code.

Reusability: The Validator interface and its implementations can be reused across different parts of the codebase, reducing code duplication and promoting consistency in validation logic. This can save development time and effort.

Composability: The chaining methods allow for easy composition of smaller, simpler validators to express complex validation rules in a concise and readable way. This makes it easier to understand and maintain the validation logic.

Flexibility: The chaining methods provide a flexible way to combine validators in different ways to meet the specific needs of a particular use case. This allows for dynamic and adaptable validation logic that can evolve over time.

Testability: The encapsulation and reusability of the Validator interface and its implementations make it easier to write unit tests for validation logic. The chaining methods also allow for more fine-grained testing of the validation logic, ensuring robustness and reliability.

Conclusion

In conclusion, incorporating a design pattern that utilizes chaining methods in a Validator interface can greatly enhance the effectiveness of code validation. This approach promotes encapsulation, reusability, composability, flexibility, and testability, resulting in more robust and maintainable code. By chaining validation methods together, we can reduce the complexity of code validation logic and make it more concise and readable. This not only helps catch errors early and adhere to requirements but also improves code organization and promotes high-quality software development practices. So, let’s embrace this design pattern in our code validation efforts to create reliable and dependable software.

Happy Coding!

--

--

Animesh Chaturvedi
Walmart Global Tech Blog

Senior Software Engineer @ Walmart | Open Source Contributor at Project Reactor | LLM Enthusiast