Since you’re here, you probably reach the point in which standard annotations like @NotNull or @Size don’t meet your expectations. Fortunately, I have good news for you. Creating a custom validation annotation is pretty easy. In this post, you will learn how to create a custom constraint annotation and a corresponding validator class. You will also see how to use Spring beans inside a custom validator.
Let’s just right into it.
Presented samples works both in Spring Boot 1.x and 2.xAdvertisement
Setup of study case
First of all, we need a sample problem that we can solve together.
For demonstration purpose, let’s consider a REST endpoint which will allow registration for new users of some application. Our goal in this tutorial is to validate login uniqueness that the client of the service will provide.
@RestController @RequestMapping("/users") public class UserController { private UserRepository userRepository; public UserController(UserRepository userRepository) { this.userRepository = userRepository; } @PostMapping public void register(@RequestBody @Valid User user) { userRepository.save(user); } }
Next, we need some data storage.
In a real application, we would use some persistent storage for the collection of our users. For instance, a relational database. Yet, for simplicity of the example, we’re going to use in-memory storage directly in the repository class.
In addition, we implement a method which based on the given login looks for a user in the registered user collection. We’re going to use it later on in our custom validator.
@Repository class UserRepository { private List<User> registeredUsers = new LinkedList<>(); void save(User user) { registeredUsers.add(user); } Optional<User> findByLogin(String login) { return registeredUsers.stream() .filter(user -> user.getLogin().equals(login)) .findFirst(); } }
Finally, we need to declare the user data model which will act as the input for our service. It will contain the login we want to check for uniqueness.
public class User { @UniqueLogin private String login; private char[] password; private User() { // no-arg Jackson constructor } public User(String login, char[] password) { Objects.requireNonNull(login); Objects.requireNonNull(password); this.login = login; this.password = password; } // getters omitted }
We annotate the login with a @UniqueLogin annotation. It doesn’t exist yet. We’re going to create our custom validation annotation the next step.
A side comment:
By default, Jackson uses reflection to set values of fields. The library requires a no-argument constructor for a class. You can make the constructor private to maintain the interface of your class unpolluted for public use and to keep Jackson working correctly.
Custom contraint annotation
New it’s time to create your first custom validation annotation.
At the first glance, the declaration of @UniqueLogin may look quite complex. But don’t get discouraged. You can find the explanation under the code listing.
@Target({ElementType.METHOD, ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy = UniqueLoginValidator.class) public @interface UniqueLogin { String message() default "{com.dolszewski.blog.UniqueLogin.message}"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
Our custom annotation is itself annotated with three other annotations. If you have ever created a custom annotation, @Target and @Retention shouldn’t be new to you. If you see them for the firs time, no worries.
The @Target annotation describes where you can apply your custom annotation. In our case, we allow using @UniqueLogin for class fields and methods. Why? Because constrains can be declared on fields, constructor parameters, and setters.
The @Retention annotation describes if the custom annotation should be available in the byte code. If so, Java can read it with the reflection mechanism.
Explaining @Constraint annotation
For you, the most interesting part is the @Constraint annotation. It actually marks our annotation for use as a validation constraint.
Let’s start with the validatedBy attribute. We use it to declare a class that will contain actual validation logic. We’re going to implement the custom validator class in a minute. If you wanted to use your custom validation annotation for several different types, you would have to register here multiple validators.
Next, when you annotate a custom annotation with @Constraint, it needs to define groups and payload attributes. We leave them blank just to fulfill the contract. If you’re interested in their purpose, I refer you to the JavaDoc.
Finally, the message attribute. We specified the key of the message that the validation framework will return when the validator finds an error.
Validation error message
We’ve already defined the error key. Now, we’re going to map it to a human-readable message.
By default, the validation framework searches for error message key mappings in the file called ValidationMessages.properties. The file should be available on the application classpath. If you don’t have one already in your app, it’s a good moment to create it and add the following key into its content.
com.dolszewski.blog.UniqueLogin.message=The given login is already in use
Custom validator implementation
Now it’s time for actual verification logic and the class that we declared in the validatedBy attribute of our custom constraint annotation.
class UniqueLoginValidator implements ConstraintValidator<UniqueLogin, String> { private UserRepository userRepository; public UniqueLoginValidator(UserRepository userRepository) { this.userRepository = userRepository; } public void initialize(UniqueLogin constraint) { } public boolean isValid(String login, ConstraintValidatorContext context) { return login != null && !userRepository.findByLogin(login).isPresent(); } }
The first thing that you should notice is the fact that the class isn’t annotated with any Spring component marker. Yet, it has a dependency on the UserRepository class which is managed by our Spring context.
Why don’t we need any @Component annotation?
The Spring framework automatically detects all classes which implement the ConstraintValidator interface. The framework instantiates them and wires all dependencies like the class was a regular Spring bean.
The ConstraintValidator interface expects two generic types. The first one is the corresponding constraint annotation. The one we created before. The second is the type of field which we want to validate with our custom validator. Since our login field is declared as String, we placed here this type.
The interface requires from us to implement two methods:
- initialize()
- isValid()
The name of the initialize() method is self-explanatory. If a validator needs some post-construct logic, here is the place to do it. In our simple case, we leave it.
The isValid() method is the place where we place the verification logic. Here, without any problem, we make use of the injected UserRepository instance.
5. Unit testing
In addition, we can test our solution. Because our validator relies upon the Spring Context, the integration test is mandatory.
@RunWith(SpringRunner.class) @SpringBootTest public class UserTest { @Autowired private UserRepository userRepository; @Autowired private Validator validator; @Test public void shouldValidateDuplicatedLogin() throws Exception { // given String login = "daniel"; User predefinedUser = new User(login, "pass".toCharArray()); userRepository.save(predefinedUser); // when User newUser = new User(login, "wrong".toCharArray()); Set<ConstraintViolation<User>> violations = validator.validate(newUser); // then assertEquals(1, violations.size()); } }
The test is pretty straightforward. Let me comment on it.
At the beginning, we populate the registered user collection in the repository with a predefined user. Next, we use the Validator abstraction injected from the Spring context to verify whether another user object with the same login as the predefined one is valid. As the result, we expect to receive one violation of constraints and that is what happens after test execution.
Check it out on your own.
6. Summary
In this article, you learned that in order to create a custom field verification, you need to create two elements. You start with a marker annotation similar to widely known constraint annotations like @NotNull or @Size that you can use in data model classes. The annotation doesn’t verify anything itself, hence we have to implement a corresponding constraint validator class. The Spring framework takes care of the rest by registering the class and injecting all declared dependencies.
As usual, you can find the whole sample application in the GitHub repository. You can also continue reading the validation series and learn about parametrized constraints or class level validation. If you have any questions or doubts, please leave a comment. Also please consider subscribing to my mailing list so you won’t miss future articles.
Hi,
have you tried this with spring-boot 1.5.1/3/4 ?
Hi Daniel. I’ve just upgraded Spring Boot version to 1.5.4 in the GitHub repository with all samples and they all work perfectly fine.
Hi Daniel, thank you for your reply. I’ll run my tests again with 1.5.4. I tryed yesterday with no success, entities and form validation didn’t work (including custom ConstraintValidator implementations ). If I remember well, the validation samples in the repository only includes properties validations not jsr validations on entities.
I’ll post again later.
Sorry, was talking about spring-boot sample repo… >.<
works fine with spring-boot 2.0.0
thank you!
Always glad to help! I’m planning to upgrade all my Github samples to Spring Boot 2 in the near future.