Configurable Model Validations with Jakarta Bean Validation, Spring Boot and Hibernate

Estafet consultants occasionally produce short, practical tech notes designed to help the broader software development community and colleagues. 

If you would like to have a more detailed discussion about any of these areas and/or how Estafet can help your organisation and teams with best practices in improving your SDLC, you are very welcome to contact us at enquiries@estafet.com 

Introduction

Data, in the modern world, is often considered the most valuable asset that companies own. It comes as no surprise that data validation is a common requirement in every software development project. If your input data isn’t accurate, then the operations you perform on it will also most likely be incorrect, which is why data validation is crucial for ensuring data consistency and integrity across applications.

During development of a solution, you might encounter a situation in which there are different data validation requirements per environment or per client. In this tech note, I will outline how you can dynamically configure Java model validations by using custom constraints and validators. You will find a step-by-step guide which can help you understand how you can tackle a similar problem. This approach will deliver the following benefits:

  • All configurable constraint parameters will be externalised and independent from each other
  • Since the validation parameters will be inside an application properties file, they can be changed without rebuilding and redeploying the application
  • If needed, you can add extra logic inside the custom constraint validators

Jakarta Bean Validation (formerly known as Java Bean Validation) is the de facto industry standard for validating Java models. The idea behind it, as its motto says, is to “constrain once, validate everywhere“. Jakarta Bean Validation allows you to add constraints on object models via built-in annotations, as well as write your own custom constraints in an extensible way.

Since Jakarta Bean Validation is just a specification, in order to use its features, an actual implementation of the API is required. Spring Boot provides support for the Jakarta Bean Validation API by using Hibernate Validator under the hood (Hibernate Validator is the reference implementation of Jakarta Bean Validation):

Spring Boot supports the Jakarta Bean Validation API, as implemented by Hibernate Validator

An easy way to quickly set up bean validations in your Spring Boot project is to include the Spring Boot Validation Starter. If you are using Maven, you need to add this to your pom.xml file:

This will automatically include the most recent compatible version of the Hibernate Validator to your project. The below snippet shows a bean validation approach that is familiar to many Java developers:

A list of all built-in Jakarta Bean Validation constraints can be found here. These are often sufficient for your everyday validation needs. However, there are certain cases when some special behaviour is required.

Configurable Validation Attributes

In the above code example, suppose you want to have different “min” and “max” size limitations for a Person’s firstName field depending on the environment. Perhaps you want to keep the “min” size to 1 on your dev environment, but you want to set it to “4” for test and production. Or perhaps you support several clients who have different validation requirements for this field. At this point, the validation constraint values are pretty much hardcoded. What can we do to make them dynamic/configurable?

The most natural thing that comes to mind is to have these values as application properties in your Spring Boot application.properties / application.yml file. Spring will dynamically pick up the different values depending on the environment:

However, if you try to use these properties directly inside the constraint annotations, you will get a compilation error:

Incompatible types. Found: ‘org.springframework.beans.factory.annotation.Value’, required: ‘int’ 

This error occurs because according to the Java Language Specification, the values of annotation parameters must be compile-time constants. Since Spring application properties are resolved at runtime, the compiler will not let you use @Value alongside bean validation annotations.

Fortunately, there is a way to bypass this limitation by defining a custom validation constraint that is capable of reading application property values.

Creating a Custom Constraint

A bean validation constraint consists of two parts:

  1. A @Constraint annotation that declares the constraint and its configurable properties
  2. An implementation of the jakarta.validation.ConstraintValidator interface that implements the constraint’s behaviour.

Below is a breakdown of the steps you need to take to create a custom annotation that behaves similarly to the built-in @Size, but reads the values for its “min”, “max” and “message” parameters from an application properties file.

Step 1: Defining the custom constraint annotation

Let’s call our annotation @CustomSize. The original Jakarta @Size constraint declares two int parameters: min and max. For @CustomSize, we will use two Strings instead: minProperty and maxProperty. These fields will hold the name of the corresponding configuration property as declared in application.yml. If we don’t provide minProperty or maxProperty when using the annotation, these parameters will default to an empty string. We will add an extra messageProperty parameter for the error message that is returned if the provided value is rejected by the constraint – this way, the content of the message can also be set as a configuration property:

To keep the example simple, the @CustomSize annotation will only target field declarations and it will not be repeatable. The 3 properties message, groups and payload are mandatory attributes that should be present, as demanded by the Jakarta Bean Validation API specification. For more information, you can refer to the reference documentation.

Notice how there is a @Constraint annotation that references a validator class: CustomSizeValidatorForCharSequence. This is the way the custom constraint declaration is associated with an actual implementation. A constraint can be validated by multiple validators, but for now we will just start with one: it will check if the length of a character sequence (such as String) is between the provided min and max values. We will implement the CustomSizeCharSequenceValidator validator in the next step.

Step 2: Implementing a custom validator

Having defined the annotation, let’s create a constraint validator that is able to validate fields with a @CustomSize annotation. As per the specification, this class needs to implement the ConstraintValidator interface:

We have injected a Spring PropertyResolver, which is used during the validator’s initialization for retrieving the min, max and message values from the application.yml file. We do several checks in the initialize() method to ensure that all 3 values will be set: if a property is missing or is not set correctly in the application.yml file, a fallback value will be used. The validateParameters() method is also called as an extra step of precaution – it will guarantee that the provided min and max attributes are valid, otherwise an IllegalArgumentException will be thrown.

The isValid() method contains the actual validation logic. Its implementation is straightforward: if the length of the character sequence field is between the resolved min and max values, then the field successfully passes validation. If the character sequence field is invalid, the custom validation message will be attached to the constraint validator context.

You may notice that if the input character sequence is null, the isValid() method immediately returns “true”. This is because the Jakarta Bean Validation specification advises to consider null values as valid. If null is not a valid value for an element, then it should be explicitly annotated with the standard @NotNull annotation.

This validator will check the length of character sequences (like String). If you want to use the @CustomSize annotation on collections, you will need to implement an extra validator that will check the amount of elements in the collection instead – similarly to Hibernate’s SizeValidatorForCollection. It is worth familiarising yourself with some of the reference validator implementations in the official Hibernate Validator repository to get a better understanding of how things work out of the box.

Step 3: Using the new constraint

After defining the constraint annotation and implementing the validator, you can now use the new constraint as follows:

For this example, we will only use the @CustomSize annotation on the firstName field.

Step 4: Adding tests

We will also need to add some tests to ensure the custom annotation and validator are working as intended. Below is a simple unit test that covers the basic functionality of the validator using Mockito as a mocking framework:

Since this is a unit test and there is no Spring application context, several objects need to be mocked, including Spring’s PropertyResolver. While setting up this test, we are manually setting the min property to 1 and the max property to 2.

We can also add a @SpringBootTest that will start up an application context with an active “test” profile, which means that the configuration in application-test.yml will be used:

The test asserts that if a Person’s firstName has an invalid size, the correct error message (as retrieved from application-test.yml) will be returned when attempting to validate the object.

Conclusion

Data quality and data integrity are vital to any organisation, especially data-driven ones that rely on analytics for business decisions or offer data as a product to customers. The first step in assuring data integrity is data validation. In a software application, it is not uncommon to have different validation requirements per environment or per client.

Here, we have given an example on how to achieve configurable model validations with Java and Spring Boot by leveraging custom Jakarta Bean Validation constraints. By creating a custom constraint annotation and validator, we are able to dynamically read validation constraints from Spring’s Application Properties. We have also given examples as to how one might go about testing the customly created annotation and validator.

By Yoana Ivanova, Consultant at Estafet

Stay Informed with Our Newsletter!

Get the latest news, exclusive articles, and updates delivered to your inbox.