Java bean validation, AKA JSR-303 is a Java standard which is used to perform validation on items inside a Java application. Validation is performed by applying “constraints” on data items. As long as the data satisfies these constraints, it will be considered valid.
Such constraints can be a specific numerical value range, being null or not null, having a specific number of items or length, among many others.
This makes bean validation a very valuable tool. Specially when programming an API. For example, you can use it to validate REST API calls. It can also be used to enforce certain data rules inside your application.
In this tutorial, we will discuss how to use Java bean validation in a Spring boot environment.
Adding Bean Validation Dependencies
If you are using Spring boot and have the “Spring-boot-starter-web” dependency, then you will transitively also pick up the required dependencies to enable bean validation. In this case, you do not need to take further actions to enable bean validation in your project.
The second option is to add the “Spring-boot-starter-validation” dependency to your project configuration. If you are using maven, then the dependency can be added to the pom.xml file as follows:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>2.2.6.RELEASE</version>
</dependency>
If you are using Gradle, then you can add the dependency as follows:
compile group: 'org.springframework.boot', name: 'spring-boot-starter-validation', version: '2.2.6.RELEASE'
If your project inherits from the “spring-boot-starter-parent” module, then you can omit the version numbers from the dependency declarations.
Finally, if you are not using Spring-boot and want to manually add the required dependencies, you can do that by adding dependencies to Java validation and your favorite JSR-303 implementation to your project’s dependencies configuration.
An example in Maven can look as follows:
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
<version>2.0.1</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.18.Final</version>
<scope>compile</scope>
<exclusions>
<exclusion>
<artifactId>validation-api</artifactId>
<groupId>javax.validation</groupId>
</exclusion>
</exclusions>
</dependency>
Make sure to use the jakarta-validation-api dependency instead of javax.validation (Jakarta EE is the new “rebrand” name for Java EE) as long as you have a compatible, recent version of Spring.
Adding constraints to a Spring Boot application
Now that we have our dependencies configured, it is time to introduce some basic constraints. We will provide a comprehensive list of available constraints in a later tutorial. For now, let us introduce the following annotations from the validation library:
- @NotNull: As the name implies, this annotation indicates that the annotated element should not be null.
- @Min: This annotation can be used on numerical data types such as byte, short, int, among others. The annotation adds a minimum value constraint on the annotated elements. The annotation can be used with the BigDecimal data type, but not with other floating point numbers such as floats and doubles.
- @Max: Similar to the @Min annotation, the @Max annotation is used with numerical data types. The constraint indicates a maximum value that the variable can have.
- @Size: This annotation is used to indicate the minimum and maximum size of arrays, collections such as lists and sets, Strings and maps.
These annotations can be added to variables such as method arguments and entity inner variables in order to apply the constraints on them.
Validating a Spring bean’s method arguments
The first option we will try in this tutorial is to add constraints to a Spring bean’s method arguments. One thing to keep in mind is that if you program to an interface, then you will need to add the constraints to the bean’s interface.
For example, if your bean’s interface is called “MyService” and the bean’s implementation class is “MyServiceImpl”, then you should add the method constraints in the “MyService” interface. Otherwise, you will get a ConstraintDeclarationException.
javax.validation.ConstraintDeclarationException:
HV000151: A method overriding another method must not redefine the parameter constraint configuration,
but method TicketManagementServiceImpl#cancelTicket(Ticket, String) redefines the configuration of TicketManagementService#cancelTicket(Ticket, String).
In this example, we will add the constraints to the interface of a service method.
void cancelTicket(@Min(1) long ticketId,
@NotNull @Size(min = 10, max = 200) String reasonForCancellation);
Let us explain the constraints in this method.
- The “ticketId” should have a value of 1 or larger.
- The “reasonForCancellation” string should not be null.
- The “reasonForCancellation” string’s size should be between 10 and 200 characters.
Before we try out our new method, we will need to annotate the bean with the @Validated annotation.
The @Validated annotation has many uses, and one of them is to indicate that a class should be validated at the method level. This allows us to “activate” the constraints that are added to the method arguments. Without the @Validated annotation, Spring will simply ignore the constraints.
Finally, please note that the @Validated constraint comes from Spring and not from the bean validation framework. If you do not intend to use it (because you are not using Spring or because of some other reason), then you will need to manually validate the constraints (more on that later).
Our interface will finally look like this:
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
@Validated
public interface TicketManagementService {
void cancelTicket(@Min(1) long ticketId,
@NotNull @Size(min = 10, max = 200) String reasonForCancellation);
//Some more Methods.....
Let us try out our interface with some tests. In the first test, we will pass a zero value in the ticketId.
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class TicketManagementServiceImplTest {
@Autowired
private TicketManagementService ticketManagementService;
@Test
public void testCancelTicket_ticketIdIsZero(){
ticketManagementService.cancelTicket(0, "testing validation");
}
This will cause a constraint violation exception as follows.
javax.validation.ConstraintViolationException: cancelTicket.ticketId: must be greater than or equal to 1
In our second test, we will pass a null argument to the method in the “reasonForCancellation” field.
@Test
public void testCancelTicket_reasonForCancellationIsNull(){
ticketManagementService.cancelTicket(2, null);
}
And the result is below:
javax.validation.ConstraintViolationException: cancelTicket.reasonForCancellation: must not be null
If we attempt to pass a string which is too long or too short, the validation framework will also complain as you will see next.
@Test
public void testCancelTicket_reasonForCancellationIsTooShort(){
ticketManagementService.cancelTicket(2, "Reason");
}
javax.validation.ConstraintViolationException: cancelTicket.reasonForCancellation: size must be between 10 and 200
Validating an entity object’s contents
We can also add constraints to entity classes in order to validate them. For example, we can add constraints to persistence models to make sure that invalid values are not saved into our database.
To do so, we can simply add the constraints to the entity class’s inner variables.
@Entity
public class Ticket{
@NotNull
@Min(1)
@Id
@GeneratedValue
public Long id;
@NotNull
@Size(min = 10, max = 75)
public String title;
Notice that we added the validation annotations @NotNull, @Min and @Size in order to enforce the corresponding constraints on our data.
In order to activate these entity constraints, we will need to use the @Valid annotation in our method argument. Let us go back to our service and add a new method.
import com.nullbeans.customerservice.incidentmanagement.data.models.Ticket;
import org.springframework.validation.annotation.Validated;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
@Validated
public interface TicketManagementService {
void cancelTicket(@NotNull @Valid Ticket ticket,
@NotNull @Size(min = 10, max = 200) String reasonForCancellation);
//Some more Methods.....
By adding the @Valid annotation, we indicate that the entity contains constraints that need to be evaluated. Please also note that we added the @NotNull constraint here because null values are “valid” by default. Therefore the annotation is needed if you also want to make sure that the caller does not provide null values.
Now, if we try to use invalid values in our ticket entity, the validation framework will reject them. Take the following test as an example.
@Test
public void testCancelTicket_ticketIdIsNull(){
Ticket ticket = new Ticket(); //We dont set any values here
ticketManagementService.cancelTicket(ticket, "testing validation");
}
In this test, we start a new instance of the “Ticket” class, but without initializing any inner variables. If we run this test, we will get the following result:
javax.validation.ConstraintViolationException: cancelTicket.ticket.id: must not be null, cancelTicket.ticket.title: must not be null
As you can see, the validation framework applied the constraints on the entity’s inner variables and threw an exception.
Validating constraints manually
Sometimes you might need to trigger validation manually without relying on Spring such as in our REST API PATCH tutorial. In these cases, you can create a Validator instance in your code and use that to validate entities.
Let us refactor our last test to validate a “Ticket” instance using a Validator.
@Test
public void ticketIdIsNullManualValidation(){
Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
Ticket ticket = new Ticket(); //We dont set any values here
Set<ConstraintViolation<Ticket>> violations = validator.validate(ticket);
violations.forEach(
ticketConstraintViolation -> {
logger.error("A violation has occured. Violation details: [{}].", ticketConstraintViolation);
}
);
//fail test here if violations are not empty
}
The javax.validation.Validator interface allows us to validate entities inside our code without relying on Spring to trigger the validation. This also gives us more control on the validation process.
If we run the test above, we will see the following log statements.
2020-04-22 08:12:47.445 ERROR 15964 --- [ main] .n.c.i.s.TicketManagementServiceImplTest : A violation has occured. Violation details: [ConstraintViolationImpl{interpolatedMessage='must not be null', propertyPath=title, rootBeanClass=class com.nullbeans.customerservice.incidentmanagement.data.models.Ticket, messageTemplate='{javax.validation.constraints.NotNull.message}'}].
2020-04-22 08:12:47.445 ERROR 15964 --- [ main] .n.c.i.s.TicketManagementServiceImplTest : A violation has occured. Violation details: [ConstraintViolationImpl{interpolatedMessage='must not be null', propertyPath=id, rootBeanClass=class com.nullbeans.customerservice.incidentmanagement.data.models.Ticket, messageTemplate='{javax.validation.constraints.NotNull.message}'}].
Validating nested objects
As we have seen, there are many ways to trigger validation. However, we have not talked about nested objects. What happens if an object that is being validated has inner objects which also contain validation constraints?
For example, if our “Ticket” entity contains an inner variable of type “UserAccount”. If the “UserAccount” entity has any constraints inside it, they will not be activated when we validate the parent entity.
@Entity
public class UserAccount {
@Min(1)
@Id
@GeneratedValue
public long id;
Depending on your use case, you may want to or not want to validate inner objects.
Imagine for example validating a persistence JPA entity with many relationships. You do not want to end up in a big validation operation every time you are modifying the parent entity.
However, if you would still want to validate nested objects, then you will need to annotate them with the @Valid annotation.
@Entity
public class Ticket{
//some code here
@Valid
@ManyToOne
private UserAccount assignedUser;
Now, let us try it out in a test.
@Test
public void testCancelTicket_userNotValid(){
Ticket ticket = generateValidTestTicket();
UserAccount userAccount = new UserAccount();
ticket.setAssignedUser(userAccount);
ticketManagementService.cancelTicket(ticket, "Because we need to try it!");
}
Here, we will get a constraint violation exception indicating that the user’s id is not valid.
javax.validation.ConstraintViolationException: cancelTicket.ticket.assignedUser.id: must be greater than or equal to 1
Validation Groups
Validation groups are a very useful feature when you want to apply different sets of constraints at different times.
For example, when you are deleting an object, you might only be interested that it’s ID is valid, but you do not care about the rest of the data. However, when you are creating a new object, you do not care what the caller put in the ID field because you will autogenerate it anyway. You will be interested in the other data fields in the entity.
Therefore, validation groups are a useful tool for manipulating the validation behavior for different use cases.
There are two things that you need to configure when using validation groups. First, you will need to define which constraints belong to which validation groups. The second thing is to choose the required validation group when validation is required.
Let us start by creating our validation groups. One for data creation, one for update, and one for deletion. To create a validation group, you need to create a class or an interface that will be used to mark the constraints.
package com.nullbeans.customerservice.incidentmanagement.data.validationgroups;
public interface CreationValidationGroup {
}
package com.nullbeans.customerservice.incidentmanagement.data.validationgroups;
public interface DeletionValidationGroup {
}
package com.nullbeans.customerservice.incidentmanagement.data.validationgroups;
public interface UpdateValidationGroup {
}
As you can see, there is nothing special about the validation groups implementation. Now, let us modify the constraints in the “Ticket” entity to reflect our new requirements.
@Data
@NoArgsConstructor
@Entity
public class Ticket{
@NotNull(groups = {DeletionValidationGroup.class, UpdateValidationGroup.class})
@Min(value = 1, groups = {DeletionValidationGroup.class, UpdateValidationGroup.class})
@Id
@GeneratedValue
public Long id;
@NotNull(groups = {CreationValidationGroup.class, UpdateValidationGroup.class})
@Size(min = 10, max = 75, groups = {CreationValidationGroup.class, UpdateValidationGroup.class})
public String title;
Notice that we added the validation groups to each of the constraints that we want triggered when the validation group is activated.
To activate a validation group, we will annotate the method parameter with the @Validated annotation instead of @Valid.
@Validated
public interface TicketManagementService {
void cancelTicket(@NotNull @Validated(UpdateValidationGroup.class) Ticket ticket,
@NotNull @Size(min = 10, max = 200) String reasonForCancellation);
Ticket createNewTicket(@NotNull @Validated(CreationValidationGroup.class) Ticket ticket);
When you now call the cancelTicket method, only the constraints that are marked with the “UpdateValidationGroup” will be triggered. All other constraints will not be evaluated.
If you will trigger the validation manually, then you can pass in the required validation group as a parameter to the validate method.
Set<ConstraintViolation<Ticket>> violations = validator.validate(ticket, UpdateValidationGroup.class);
Summary
In this tutorial, we discussed how to use Java bean validation inside a Spring-boot application. We discussed how to define and apply validation constraints to data objects and how to use validation groups to specify different sets of constraints in different use-cases.
If you have any questions or if you liked this tutorial, then please let us know in the comments below.
Leave a Reply
You must be logged in to post a comment.