Spring MVC Custom Validator
In this post, we will learn about using Spring MVC Custom Validator. We will explore as to how to create and use your own custom validator.
Introduction
Spring MVC provides first-hand support for JSR-303 Bean Validation API. We can use this API to validate our input data. Bean Validation API ship with a number of different validators which can be used with the use of simple annotation, however in case we need to validate data based on custom business rules, we have the option to create our own custom validator and hook it with Spring MVC validation framework.
We will be exploring Spring MVC Custom Validator and see how to hook it in our application.
1. Maven Setup
In order to create and use the custom validator, we need to validation API and JSR-303 compliance validation API in our class path, we will be using Hibernate validator in our example. To start we need to add following entry in our pom.xml
file
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>1.1.0.Final</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>5.4.1.Final</version>
</dependency>
We will be using Spring Boot for this post, so we will be using Spring Boot starters for add validation API in our class path. To use Hibernate validation API in your Spring Boot application, you need to add the following starter in your pom.xml
file
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
2. Custom Class Level Validation
To understand it, let’s take a simple example where we want to create a custom validator to validate two fields in our input form to make sure they are equal. (let’s assume we want to confirm customer email address)
2.1 Annotation
We will start by creating a new @interface
to define our new custom constraint.
@Constraint(validatedBy = FieldMatchValidator.class)
@Documented
@Target({TYPE, ANNOTATION_TYPE})
@Retention(RUNTIME)
public @interface FieldMatch {
String message() default "Fields are not matching";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
/**
* @return The first field
*/
String first();
/**
* @return The second field
*/
String second();
/**
* Defines several <code>@FieldMatch</code> annotations on the same element
*
* @see FieldMatch
*/
@Target({TYPE, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Documented
@interface List {
FieldMatch[] value();
}
}
With @Constraint
annotation, we are specifying which class will actually be performing validation, in our case FieldMatchValidator
class will be doing the actual validation, message()
will define the message which will be shown to the customer when validation fails, this is the default message and it can be changed through configuration (we will see that shortly).
2.2 Validation Class
Let’s have a look at the actual validation class which will be responsible for performing our custom validation.
public class FieldMatchValidator implements ConstraintValidator<FieldMatch, Object> {
private String firstFieldName;
private String secondFieldName;
public void initialize(final FieldMatch constraintAnnotation) {
this.firstFieldName = constraintAnnotation.first();
this.secondFieldName = constraintAnnotation.second();
}
public boolean isValid(final Object value, final ConstraintValidatorContext context) {
try {
final Object firstObj = PropertyUtils.getProperty(value, this.firstFieldName);
final Object secondObj = PropertyUtils.getProperty(value, this.secondFieldName);
return firstObj == null && secondObj == null || firstObj != null && firstObj.equals(secondObj);
} catch (final Exception ex) {
//LOG.info("Error while getting values from object", ex);
return false;
}
}
}
Our Validation class implements ConstraintValidator
interface and should implement isValid
method, all our validation logic will go inside this method.In our custom validation, we are fetching values from the both fields and matching them to return a boolean value.
3. Validation in Action
It’s time to see our custom validation in action, to demonstrate it, we will be using a simple Spring Boot based web application where we will be validating 2 fields and will throw an error in case both fields do not match with each other.
3.1 Sample Controller
@Controller
public class CustomValidatorController {
@RequestMapping("/custom-validator")
public String hello(Person person) {
return "customValidator";
}
@PostMapping("/custom-validator")
public String demoValidation(@Valid Person person, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return "customValidator";
}
return "redirect:/results";
}
@RequestMapping("/success")
public String welcome() {
return "welcome";
}
}
3.2 Person Object
@FieldMatch(first = "email", second = "confirmEmail", message = "Email does not match")
public class Person {
@NotNull
private String name;
private String email;
private String confirmEmail;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getConfirmEmail() {
return confirmEmail;
}
public void setConfirmEmail(String confirmEmail) {
this.confirmEmail = confirmEmail;
}
}
As you can see, we have added @FieldMatch
annotation on the top of our Person class which will compare email
and confirmEmail
fields and in case they don’t match, it will throw a validation error.
3.3 View
<html>
<body>
<form action="#" th:action="@{/custom-validator}" th:object="${person}" method="post">
<table>
<p th:if="${#fields.hasErrors('global')}" th:errors="*{global}">
Incorrect date
</p>
<tr>
<td>Name:</td>
<td><input type="text" th:field="*{name}" /></td>
<td th:if="${#fields.hasErrors('name')}" th:errors="*{name}">Name Error</td>
</tr>
<tr>
<td>Email:</td>
<td><input type="text" th:field="*{email}" /></td>
<td th:if="${#fields.hasErrors('email')}" th:errors="*{email}">Email Error</td>
</tr>
<tr>
<td>Confirm Email:</td>
<td><input type="text" th:field="*{confirmEmail}" /></td>
<td th:if="${#fields.hasErrors('confirmEmail')}" th:errors="*{confirmEmail}">Email Error</td>
</tr>
<tr>
<td><button type="submit">Submit</button></td>
</tr>
</table>
</form>
</body>
</html>
If we run our application and add non-matching email id’s, we will get “Email does not match” error on the front end.
We can also define custom validation on the field level. We need to use the same approach to define field level annotation.
4. Custom Field Level Validation
We can also define custom validation on the field level. We need to use the similar approach to define field level annotation.
4.1 Annotation
@Constraint(validatedBy = CountryMatchValidator.class)
@Documented
@Target( { ElementType.METHOD, ElementType.FIELD })
@Retention(RUNTIME)
public @interface CountryMatch {
String message() default "Invalid Country";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
/**
* Defines several <code>@FieldMatch</code> annotations on the same element
*
* @see FieldMatch
*/
@Target({TYPE, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Documented
@interface List {
FieldMatch[] value();
}
}
4.2 Validation Class
public class CountryMatchValidator implements ConstraintValidator<CountryMatch, String> {
/**
* Initializes the validator in preparation for
* {@link #isValid(Object, ConstraintValidatorContext)} calls.
* The constraint annotation for a given constraint declaration
* is passed.
* <p/>
* This method is guaranteed to be called before any use of this instance for
* validation.
*
* @param constraintAnnotation annotation instance for a given constraint declaration
*/
@Override
public void initialize(CountryMatch constraintAnnotation) {
}
/**
* Implements the validation logic.
* The state of {@code value} must not be altered.
* <p/>
* This method can be accessed concurrently, thread-safety must be ensured
* by the implementation.
*
* @param country country
* @param context context in which the constraint is evaluated
* @return {@code false} if {@code value} does not pass the constraint
*/
@Override
public boolean isValid(String country, ConstraintValidatorContext context) {
try {
return country == null && country.equals("US");
} catch (final Exception ex) {
//LOG.info("Error while getting values from object", ex);
return false;
}
}
}
4.3 Person Object
@FieldMatch(first = "email", second = "confirmEmail", message = "Email does not match")
public class Person {
@NotNull
private String name;
private String email;
private String confirmEmail;
@CountryMatch(message = "Country should be US")
private String country;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getConfirmEmail() {
return confirmEmail;
}
public void setConfirmEmail(String confirmEmail) {
this.confirmEmail = confirmEmail;
}
public String getCountry() {
return country;
}
public void setCountry(String country) {
this.country = country;
}
}
In order to initiate Spring MVC JSR validation, you need to annotate your model with @Valid
annotation in your controller
5. Summary
In this post, we learn about using Spring MVC Custom Validator. We covered how to create and use class and field level validation.
All the code of this article is available Over on Github. This is a Maven-based project.
I don’t understand this statement
“return country == null && country.equals(“US”);”
How does this work? IMO this would also return false.