Java Bean Validation: Applying Constraints Programmatically
Learn how to configure a Java bean validator in order to apply constraints programmatically within a Spring application.
Join the DZone community and get the full member experience.
Join For FreeImagine you are working on a project that includes several entities implemented as part of the system. According to the required business logic, you need to enforce specific validation rules. However, these classes are provided as a third-party dependency, which means they cannot be modified and as a result cannot be annotated directly.
To tackle this scenario, we will explore a feature of the Hibernate Validator known as 12.4. Programmatic constraint definition and declaration from the documentation, and demonstrate how it can be integrated with the Spring Framework.
The source code referenced in this article is available on GitHub via this link.
Use Case Description
Let's introduce a User
entity to better understand our use case. This entity will act as a third-party dependency throughout our example:
package com.example.bean.validation.entity;
public class User {
private Long id;
private String name;
// Setters ommited
}
As observed, it is a Java POJO. However, remember that we cannot modify its code due to our specific use case constraints.
To validate the User
's id
, we have created a custom annotation named UserId
:
package com.example.bean.validation.constraint;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import jakarta.validation.ReportAsSingleViolation;
import jakarta.validation.constraints.NotNull;
import org.hibernate.validator.constraints.Range;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Documented
@NotNull
@Range(min = 1)
@ReportAsSingleViolation
@Constraint(validatedBy = {})
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER, ElementType.METHOD})
public @interface UserId {
String message() default "${validatedValue} must be a positive long";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
Furthermore, according to our business logic, the User
's name
must not be null. So, we are going to apply the existing NotNull
constraint to this field.
Validator Setup
Now, let's examine our validator configuration, which performs the necessary operations to meet our use case requirements:
package com.example.bean.validation.conf;
import com.example.bean.validation.constraint.UserIdDef;
import com.example.bean.validation.entity.User;
import jakarta.validation.Validation;
import jakarta.validation.Validator;
import org.hibernate.validator.HibernateValidator;
import org.hibernate.validator.HibernateValidatorConfiguration;
import org.hibernate.validator.cfg.ConstraintMapping;
import org.hibernate.validator.cfg.context.TypeConstraintMappingContext;
import org.hibernate.validator.cfg.defs.NotNullDef;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.validation.beanvalidation.SpringConstraintValidatorFactory;
@Configuration
class ValidationConf {
@Bean
Validator validator(AutowireCapableBeanFactory autowireCapableBeanFactory) {
HibernateValidatorConfiguration conf = Validation.byProvider(HibernateValidator.class).configure();
ConstraintMapping constraintMapping = conf.createConstraintMapping();
TypeConstraintMappingContext<User> context = constraintMapping.type(User.class);
context.field("id").constraint(new UserIdDef());
context.field("name").constraint(new NotNullDef());
return conf.allowOverridingMethodAlterParameterConstraint(true)
.addMapping(constraintMapping)
.constraintValidatorFactory(new SpringConstraintValidatorFactory(autowireCapableBeanFactory))
.buildValidatorFactory()
.getValidator();
}
}
Here, through the TypeConstraintMappingContext
, we assign the necessary annotations to the User
's fields by providing their respective constraint definition classes. For our custom UserId
, its constraint definition UserIdDef
must be implemented by us, as shown:
package com.example.bean.validation.constraint;
import org.hibernate.validator.cfg.ConstraintDef;
public class UserIdDef extends ConstraintDef<UserIdDef, UserId> {
public UserIdDef() {
super(UserId.class);
}
}
For the built-in NotNull
constraint, the corresponding definition class NotNullDef
is readily available.
Applying Validation Logic
In addition to our previous configurations, we have implemented a component using the Spring-driven method validation approach:
package com.example.bean.validation.component;
import com.example.bean.validation.entity.User;
import jakarta.validation.Valid;
import org.springframework.stereotype.Component;
import org.springframework.validation.annotation.Validated;
@Validated
@Component
public class SomeComponent {
public void handleUser(@Valid User user) {
System.out.println("Got validated user " + user);
}
}
Testing Constraints
To validate the SomeComponent
, let's review test scenarios implemented to clearly understand the validation logic applied:
package com.example.bean.validation.component;
import com.example.bean.validation.entity.User;
import jakarta.validation.ConstraintViolationException;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
@SpringBootTest
class SomeComponentTest {
@Autowired
private SomeComponent someComponent;
@Test
void provideInvalidUser() {
User user = new User();
user.setId(-100L);
user.setName(null);
assertThatThrownBy(() -> someComponent.handleUser(user))
.isInstanceOf(ConstraintViolationException.class)
.hasMessageContaining("handleUser.arg0.id: -100 must be a positive long")
.hasMessageContaining("handleUser.arg0.name: must not be null");
}
@Test
void provideValidUser() {
User user = new User();
user.setId(1L);
user.setName("Bob");
assertDoesNotThrow(() -> someComponent.handleUser(user));
}
}
Final Words
In this article, we have explored the use case of programmatically annotating fields of an entity. While this approach addresses our specific needs, other more complex scenarios are also possible. For additional information and advanced techniques, refer to the 12.4. Programmatic constraint definition and declaration in the Hibernate Validator documentation (linked earlier in the article).
Opinions expressed by DZone contributors are their own.
Comments