Spring MVC Custom Validation Annotations
Last tutorial, I showed how to validate a form using annotations. This works great for simple validations, but eventually, you’ll need to validate some custom rules that aren’t available in the out-of-the-box annotations. For example, what if you need to validate that a user is over 21 years old, calculated based off their input birthdate, or, maybe you need to validate that the user’s phone area code is in Nebraska, USA. This tutorial with full source code will show how to create custom validation annotations that you can use along-side the JSR-303 and Hibernate Validator annotations we explored in the last tutorial.
You can grab the code for this tutorial on GitHub if you want to follow along.
For this example, let’s say we have a form with a phone number field and a birthdate field, and we want to validate the the phone number is valid (simple check for format) and that the user was born in 1989. There are no out-of-the-box annotations that support these (as far as I know), so we will write custom validation annotations which we can then re-use, just like the built-in JSR-303 ones.
When we are done, we will apply our annotations to our form object, like so:
public class Subscriber { ... @Phone private String phone; @Year(1989) private Date birthday; // getters setters ... }
Let’s get started with the @Phone annotation. We will be creating two classes: Phone
, which is the annotation, and PhoneConstraintValidator
which contains the validation logic. The first step is to create the Phone
annotation class:
@Documented @Constraint(validatedBy = PhoneConstraintValidator.class) @Target( { ElementType.METHOD, ElementType.FIELD }) @Retention(RetentionPolicy.RUNTIME) public @interface Phone { String message() default "{Phone}"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
The code above is mostly just boiler-plate. The three methods in the annotation are required by the JSR-303 spec. If our annotation accepted any arguments, we would have defined them there as methods. We will see this in our next annotation later in this tutorial. The most important part of the class above is the @Constraint
annotation on the class which specifies that we will use our PhoneConstraintValidator
class for the validation logic. The message()
method defines how the message is resolved. By specifying “{Phone}”, we can override the message in a Spring resource bundle using the Phone
key (see my other validation tutorial for details about messages).
Now, we define the constraint validator:
public class PhoneConstraintValidator implements ConstraintValidator<Phone, String> { @Override public void initialize(Phone phone) { } @Override public boolean isValid(String phoneField, ConstraintValidatorContext cxt) { if(phoneField == null) { return false; } return phoneField.matches("[0-9()-\.]*"); } }
Let’s look at the above code. The templated type of the superclass takes two types: the type of the annotation it supports, and the type of the property it validates (in this example, Phone, String).
The “initialize” method is empty here, but it can be used to save data from the annotation, as we will see below when we define our other annotation.
Finally, the actual logic happens in the “isValid” method. The field value is passed in as the first argument, and we do our validation here. As you can see, I am just validating that the phone number only contains numbers, parentheses or dashes.
That’s it for this annotation! The annotation can now be used on a field as shown above on our form object.
Now, let’s do our second annotation. This one is a little contrived – we will validate that the user’s birthdate is in 1989. In the future, we may need to validate dates are in other years, though, so rather than create an annotation that validates the year to be 1989, we will let it take an argument to specify the year to validate against. Example usage:
@Year(1989) private Date birthDate;
Now, the annotation:
@Documented @Constraint(validatedBy = YearConstraintValidator.class) @Target( { ElementType.METHOD, ElementType.FIELD }) @Retention(RetentionPolicy.RUNTIME) public @interface Year { int value(); String message() default "{Year}"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
Notice the “value()” method. This exposes the “value” argument of the annotation which we will use to pass the year that the annotation should validate against. The rest of the code is mostly boilerplate
Now, the constraint validator:
public class YearConstraintValidator implements ConstraintValidator<Year, Date> { private int annotationYear; @Override public void initialize(Year year) { this.annotationYear = year.value(); } @Override public boolean isValid(Date target, ConstraintValidatorContext cxt) { if(target == null) { return true; } Calendar c = Calendar.getInstance(); c.setTime(target); int fieldYear = c.get(Calendar.YEAR); return fieldYear == annotationYear; } }
The first thing to notice, is that, this time, we are saving the year passed into the annotation as a member variable of the constraint validator class. This allows us to access the value in our “isValid” method.
The isValid method is pretty straightforward code wrestling with the obnoxious Date/Calendar API’s to validate that the value of the annotated field matches the year that the validation annotation specified (I may post an example using JodaTime sometime if I get around to it). And now, if we start up our web application, our two validations are in place and ready to be used!
That’s all. Did I miss anything? Have questions? Let me know in the comments.
Full Source: ZIP, GitHub
To run the code from this tutorial: Must have Gradle installed. Clone the GitHub repo or download the ZIP and extract. Open command prompt to code location. Run gradle jettyRunWar. Navigate in browser to http://localhost:8080.
Nice post.
How can i add additional attributes to my custom annotation ?
For ex Year(day=””, month=””)
Can you send me simple explained example of spring validation without hibernate?