Spring Bean Validation Example (JSR-303) in Kotlin

Many developers need a solution to validating forms. Spring MVC supports bean validation (JSR-303) which is an easy to use annotation driven framework that is used to validate objects. Spring utilizes bean validation throughout the entire application framework, allowing developers to annotation one or more objects and ensure they are valid from the controller all the way to the data access layer.

Since Kotlin is compatible with Java libraries, this post demonstrates how to use bean validation along with Spring MVC to validate a user registration form. Here is a screen shot of the form we are going to validate.
WelcomeScreen copy

Step 1—Define a Form Object

We will use a POJO that carries data from the view to the server. Kotlin makes this really easy because it provides data classes.

data class User(@get: NotBlank (message = "{first_name.required}")
                    var firstName : String = "",

                @get: NotBlank (message = "{last_name.required}")
                    var lastName : String = "",

                @get: NotBlank (message="{email.required}")
                @get: Email (message = "{email.invalid}")
                    var email : String = "",

                @get: NotBlank (message="{phone.required}")
                    var phone : String = "",

                @get: NotBlank (message="{address.required}")
                    var address : String = "",

                @get: NotBlank (message="{city.required}")
                    var city : String = "",

                @get: NotBlank (message="{state.required}")
                @get: Size(min=2, max=2, message = "{state.size}")
                    var state : String = "",

                @get: NotBlank (message="{zip.required}")
                    var zip : String = "")

You will notice a couple of things in the class. The first is the @get [annotation] annotation. Kotlin generates getter/setter methods in the Java bytecode. When we use the @get annotation followed by the annotation, Kotlin will place the target annotation on the getter method. So for example, @get: NotBlank is telling the Kotlin compiler to place the @NotBlank annotation on the generated getter method.

Next you will notice the message argument that is passed to the NotBlank annotation. In the first example, we have message = “{first_name.required}”. The message parameter takes a String that is displayed to the user when validation fails. If we use expression language, Spring boot will look for a ValidationMessages.properties file that contains the actual message. This is useful when making localized applications because you can have a different ValidationMessages file per language.

In this example, we are using three different Hibernate Validator annotations.

  • NotBlank—Valid if a String is not blank. Fails is the String is blank or null.
  • Email—Valid if the string field contains a valid email address
  • Size—Valid if the string has at least min number of characters and less than max characters

Step 2—ValidationMessages.properties

If you choose to use a ValidationMessages.properties file, Spring Boot will look for the actual message inside of a ValidationMessages.properties file in the applications classpath. I find this to be the preferred approach because it allows develoeprs to easily localize an application if needed. Here is the properties file we are using in this example.

first_name.required="First name is required"
last_name.required="Last name is required"
email.required="Please enter your email address"
email.invalid="Please provide a valid email address"
phone.required="Please enter a phone number"
address.required="Please enter your address"
city.required="Please enter a city"
state.required="Please enter a state"
state.size="Please use 2 letter state code"
zip.required="Enter a zip code"

The keys of the file are the same as when is passed to the message parameter of each bean validator annotation. The value is what is ultimately displayed to the user.

Step 3-Controller

Spring MVC uses a controller class. Here is the code for our controller in this example.

@Controller
@RequestMapping("/")
@Scope(WebApplicationContext.SCOPE_REQUEST)
class ValidatorController(@Autowired val registrationService: RegistrationService) {

    @RequestMapping(method = arrayOf(RequestMethod.GET))
    fun doGet(model : Model) : String {
        model.addAttribute("user", User())
        return "index"
    }

    @RequestMapping(method = arrayOf(RequestMethod.POST))
    fun doPost(@Valid user : User,  //The @Valid annotation tells Spring to Validate this object
               errors : Errors)  //Spring injects this class into this method. It will hold any
                                //errors that are found on the object
            : String {
        val result : String
        when {
            //Test for errors
            errors.hasErrors() -> result = "index"
            else -> {
                //Otherwise proceed to the next page
                registrationService.user = user
                result = "redirect:/congrats"
            }
        }
        return result
    }
}

The first method is doGet which is mapped to an HTTP Get request. Nothing fancy here for those readers who are famailier with Spring MVC (This post explains Spring MVC for users who are not famalier with Spring MVC). All we do here is create a new instance of the User class and pass it to the view.

The doPost is the method that we care about for this post. This method has two parameters. The first one is a User argument that is annotated with @Valid. The other one is an Errors object. These two argument work in tandem to validate a Spring MVC form.

The User object is bound to the web form. Each field in the form is mapped to a property on the User class. When the form is submitted, Spring MVC carries this object from the View to the Controller. However, if the parameter has the @Valid annotation, then the User object make a stop and is examaned by a class that impelements javax.validation.Validator. Should the validation report any errors, they are gathered together in the Errors object that is injected into this method.

The errors.hasErrors() method returns true if the user entered invalid input into the form. We can return the name of the page that was submitted and report the errors back to the user if that is the case. Here is what it looks like.
Invalid copy

On the other hand, we can redirect the user to the next page if everything is ok (errors.hasErrors() == false).
Congrats copy

Step 4—The View

Of course, we need to setup our web page properly in order to actually see the errors. Since I am a big fan of Twitter’s Bootstrap, I use their form validation framework to make a nice front-end display to the errors message.

Here is a code snippet of a field on the form.
field
We use a teneray expression to check each field if it has errors th:class="${#fields.hasErrors('firstName') ?'form-group has-error' : 'form-group'}". If the field has an error, we use bootstraps has-error css class to get the red outline. Otherwise, we don’t add that class and just use form-group. In case you are wondering, the fields object is the same Errors object that we were using in our server code. This is just another rendition of the same object.

The next thing to do is print out each error. This can be done very easily by using the th:each tag and assigning each message to the err variable: th:each="err: ${#fields.errors('firstName')}" class="help-block" th:text="${err}". When executed, this code will look up the message (found in ValidationMessages.properties) for each error and write is out to a span tag.

When run, each field that has an error will have its textbox outlined and the error message printed below the text box.

Getting the Code

You can get the complete code for this project at my github page.

Result

Here is the final result

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s