Spring Custom Validator by example

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.x
Advertisement

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.

Facebooktwittergoogle_plusredditlinkedinmail
Advertisement