lishman levelup
«previous  next»


Getting Started
Form Processing
IoC Container
Hibernate ORM



Spring MVC Form Validation


Spring supports flexible data binding for populating arguments of annotated handler methods.
@RequestMapping(params = "update", method = RequestMethod.POST)
public String update(Country country, BindingResult result, SessionStatus status) {
  ..
}
In this case, Spring automatically populates the Country object using the data submitted in the HTML form.

Customizable Data Binding

Data binding is configured using the WebDataBinder class. Spring injects an instance of this class into any controller method that has been annotated with @InitBinder. This object is then used to define the data binding rules for the controller.
@InitBinder
public void initBinder(WebDataBinder dataBinder) {

  dataBinder.setDisallowedFields(new String[] {"id"});

  dataBinder.setRequiredFields(new String[] {"name", "area", "population", "currency"});

  dataBinder.registerCustomEditor(String.class, new StringTrimmerEditor(false));

  SimpleDateFormat dateFormat = new SimpleDateFormat("dd-MMM-yyyy");
  dateFormat.setLenient(false);
  dataBinder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, true));
}
setDisallowedFields() registers the fields that are not allowed for binding. This example makes sure that the user does not tinker with the 'id' field, in order to make potentially unauthorized changes.

The setRequiredFields() method lists form fields that are mandatory. An error will be raised if any values for items in the list are missing.

The StringTrimmerEditor() is a PropertyEditor which removes leading and trailing whitespace from a String.

The CustomDateEditor() is a PropertyEditor which uses the SimpleDateFormat class to define the date format that must be entered by the user.

Manual Validation

The above settings will automatically take care of many of the common types of form validation, but for more fine grained control, we can use a bespoke validator such as this:
public class CountryValidator {

  private WorldService worldService = new MockWorldService();

  public void validate(Country country, Errors errors) {

    if (country.getArea() != null && country.getArea() <= 0) {
      errors.rejectValue("area", "validation.negative", "must be > 0");
    }

    if (country.getPopulation() != null && country.getPopulation() <= 0) {
      errors.rejectValue("population", "validation.negative", "must be > 0");
    }

    if (!errors.hasFieldErrors("name")) {
      Country existingCountry = worldService.getCountryByName(country.getName());
      if (existingCountry != null &&
          (country.isNew() ||  country.getId() != existingCountry.getId())) {
        errors.rejectValue("name", "validation.exists", "exists");
      }
    }
  }
}
The Errors object, which is passed into the validate() method, stores information about data-binding and validation errors. We can add our own bespoke error messages to the data-binding error messages that Spring may or may not have generated (based on the criteria we specified in initBinder()).

The validate() method is called directly from the controller class like so:
@RequestMapping(params = "update", method = RequestMethod.POST)
public String update(Country country, BindingResult result) {
  countryValidator().validate(country, result);
  if (result.hasErrors()) {
    return "countryForm";
  } else {
    worldService.saveCountry(country);
    status.setComplete();
    return "redirect:countryList.html";
  }
}
Info The BindingResult parameter must be positioned directly after the corresponding model argument that is being validated.
An instance of BindingResult (which implements the Errors interface) is injected into our annotated handler method by Spring, and passed down to the validate() method of our validator. This way, we use the same object to store Spring generated errors, as well as custom validation errors.

Error Codes

The rejectValue() method is used to add a validation error to the Errors object.

The first parameter identifies which field the error is associated with. The second parameter is an error code which acts a message key for the messages.properties file (or messages_en.properties or messages_fr.properties etc, if these are being used).
required=is required
validation.exists=already exists
validation.negative=must be positive

typeMismatch.java.util.Date=must be a valid date
typeMismatch.java.lang.Integer=must be a valid number
typeMismatch.java.lang.Long=must be a valid number
The 'required' entry is a special code used by Spring when a field that is specified in the setRequiredFields() method, is not supplied.

The 'typeMismatch' error codes are used by Spring when the user specifies a value which is inappropriate for the type, such as 'xyz' in a number field. See DefaultMessageCodesResolver for more details.

The third parameter of rejectValue() represents the fallback default message, which is displayed if no matching error code is found in the resource bundle.

Displaying Errors

We include the <form:errors> tag along side each of the <form:input> tags in our JSP page.
<tr>
  <td><spring:message code="country.name"/></td>
  <td>
    <form:input path="name" size="20" maxlength="50" />
    <form:errors path="name" cssClass="errors"/>
  </td>
</tr>

<tr>
  <td><spring:message code="country.area"/></td>
  <td>
    <form:input path="area" size="8" maxlength="8" />
    <form:errors path="area" cssClass="errors"/>
  </td>
</tr>

<tr>
  <td><spring:message code="country.population"/></td>
  <td>
    <form:input path="population" size="10" maxlength="10" />
    <form:errors path="population" cssClass="errors"/>
  </td>
</tr>

<tr>
  <td><spring:message code="country.updatedOn"/></td>
  <td>
    <form:input path="populationLastUpdated" size="12" maxlength="12" />
    <form:errors path="populationLastUpdated" cssClass="errors"/>
  </td>
</tr>

<tr>
  <td><spring:message code="country.currency"/></td>
  <td>
    <form:input path="currency" size="20" maxlength="50" />
    <form:errors path="currency" cssClass="errors"/>
  </td>
</tr>
This way, if the countryForm view is redisplayed, and the BindingResult object contains validation errors, then the screen will look something like this:

Validation Errors
»