This site uses cookies. Continue to use the site as normal if you are happy with this, or read more about cookies and how to manage them.

×

This site uses cookies. Continue to use the site as normal if you are happy with this, or read more about cookies and how to manage them.

×

Combining Server-Side Validation with Unobtrusive JavaScript Validation

Matt recently discussed the importance of server-side validation. All web applications must perform validation of input on the server since you cannot rely on client-side validation. You do not have any control over the client.

For example:

  • A caller may be using a programmatic HTTP client to call your services. You need to be able to validate the input since the caller is using their own client software and not yours. An argument here is that programmatic clients should be using a specific API rather than your 'HTML' interface. However, there is a movement to use simple semantic HTML as the content type for RESTful APIs. This is a very useful approach for modern web application and is described here, or you can watch my presentation about this on YouTube here.
  • Your end user may have disabled JavaScript on their browser. Many sites decide not to support such users forcing them to enable it to use the site. However, in my opinion we should develop sites that will still function and degrade gracefully without JavaScript. This approach boosts both accessibility for disabled users and crawlability for search engine.

As well as these legitimate cases, you have to guard against malicious use. Without validation a malicious user can send any data, which could corrupt your database. For example, initiating a Cross Site Scripting attack.

In order for the site to degrade gracefully, we want to return markup that shows our server-side validation errors, which anyone using a browser without JavaScript enabled will see (and will fit within the look and feel of the site). However, we usually also want to provide a richer experience for users who do have JavaScript enabled, specifically dynamic field validation that gives instant feedback about problems without a round trip to the server. Integrating static server-side errors with dynamic client-side errors can cause problems. For example, you could end up with both server-side errors and client-side errors visible at the same time; or the user could fix the validation issues but still see the static server-side error which would be confusing.

In this post, I'm going to discuss how to combine server-side validation (using Spring MVC and JSR 303 bean validation) with unobtrusive client-side validation (using Bootstrap and BootstrapValidator). I'll work through a simple example and finally wrap up with the main salient points.

Let's start with server-side validation and define a model bean that can be validated using JSR 303 validation:

public class Company {
  @NotBlank(message = "The company name is required.")
  @Size(max = 50, message = "The company name can have at most {max} characters.")
  private String name;
  @Size(max = 255, message = "The company address can have at most {max} characters.")
  private String address;
  @Email(message = "The email address is not valid.")
  @Size(max = 255, message = "The email address can have at most {max} characters.")
  private String email;
  public Company() {
  }
  // setters, getters, equals, hashCode, toString
  ...
}

Here we define the validation requirements for the fields of our model using annotations. They all have a maximum length and the name is a mandatory field and the email address must be valid.

Now, let's have a look at our controller for creating companies.

@Controller
@Scope("request")
public class CompanyController {
  @RequestMapping(value = "/companies/add", method = RequestMethod.GET,
      produces = MediaType.TEXT_HTML_VALUE)
  public ModelAndView createForm() {
    Map<String, Object> model = new HashMap<>();
    model.put("company", new Company());
    return new ModelAndView("companyCreateView", model);
  }
  @RequestMapping(value = "/companies", method = RequestMethod.POST,
      consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
  public ModelAndView create(
      @Valid @ModelAttribute("company") Company company,
      BindingResult errors)
  {
    if (errors.hasErrors()) {
      return new ModelAndView("companyCreateView");
    }
    // Validated successfully so create company
    ...
  }
  ...
}

The first method gets the page with the create company form, initialising it with an empty company object.

The second method is the meat. This is the create method that the form posts to.

We specify the company input parameter and annotate it with @Valid so that validation will be triggered. The BindingResult parameter must be specified directly after the object to be validated to receive any validation errors.

We can then interrogate the binding result object to determine if there were validation problems and if so we return to the original view to allow the user to fix their errors.

I've omitted the code for actually creating the company in the database and returning to an appropriate page for brevity’s sake.

The final part of the server-side validation is the form itself, defined in companyCreateView.jsp. In this example, we're using Bootstrap for the styling and BootstrapValidator for the client-side validation, so the page we render is designed around those.

<%@taglib prefix="form" uri="http://www.springframework.org/tags/form"%>
<!DOCTYPE html>
<html lang="en">
<head>
  <title>Add Company</title>
  <link href="css/bootstrap.css" rel="stylesheet">
  <link href="css/bootstrapValidator.css" rel="stylesheet">
</head>
<body>
  <section class="container">
    <div class="row">
      <div class="col-md-12">
        <form:form commandName="company" cssClass="form-horizontal"
            method="post" action="/companies" role="form">
          <h1 class="col-md-offset-2 col-md-10">Add Company</h1>
          <div class="form-group <strong><spring:bind path="name"><c:if test="${status.error}">has-error has-feedback</c:if></spring:bind></strong>">
            <label for="companyName" class="col-md-2 control-label">Name
              <span class="required">*</span></label>
            <div class="col-md-10">
              <form:input id="companyName" path="name" type="text" maxLength="50"
                required="required" cssClass="form-control" placeholder="Enter company name..."/>
              <spring:bind path="name"><c:if test="${status.error}">
                <i class="form-control-feedback glyphicon-remove glyphicon static-error"
                  style="display: block;"></i>
                <small class="help-block static-error" style="display: block;">
                  ${status.errorMessage}</small>
              </c:if></spring:bind>
            </div>
          </div>
          <div class="form-group <strong><spring:bind path="address"><c:if test="${status.error}">has-error has-feedback</c:if></spring:bind></strong>">
            <label for="companyAddress" class="col-md-2 control-label">Address</label>
            <div class="col-md-10">
              <form:textarea id="companyAddress" path="address" rows="5" maxLength="255"
                cssClass="form-control" placeholder="Enter company address..."/>
              <spring:bind path="address"><c:if test="${status.error}">
                <i class="form-control-feedback glyphicon-remove glyphicon static-error"
                  style="display: block;"></i>
                <small class="help-block static-error" style="display: block;">
                  ${status.errorMessage}</small>
              </c:if></spring:bind>
            </div>
          </div>
          <div class="form-group <strong><spring:bind path="email"><c:if test="${status.error}">has-error has-feedback</c:if></spring:bind></strong>">
            <label for="companyEmail" class="col-md-2 control-label">Email</label>
            <div class="col-md-10">
              <form:input id="companyEmail" path="email" type="email" maxLength="255"
                cssClass="form-control" placeholder="Enter company email..."/>
              <spring:bind path="address"><c:if test="${status.error}">
                <i class="form-control-feedback glyphicon-remove glyphicon static-error"
                  style="display: block;"></i>
                <small class="help-block static-error" style="display: block;">
                  ${status.errorMessage}</small>
              </c:if></spring:bind>
            </div>
          </div>
          <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
              <button type="submit" id="create" class="btn btn-primary">Add</button>
            </div>
          </div>
        </form:form>
      </div>
    </div>
  </section>
  <script src="js/jquery.min.js"></script>
  <script src="js/bootstrap.min.js"></script>
  <script src="js/bootstrapValidator.min.js"></script>
  <script src="js/company.js"></script>
  <script type="text/javascript">
    $(document).ready(function() {
      // remove server-side static errors
      $(".static-error").remove();
      var companyForm = $('#companyForm')
      // initialise bootstrap validator based on options defined in company.js<strong>
      companyForm.bootstrapValidator(companyFormValidatorOptions);
      // activate validation immediately on fields that failed server-side validation.<strong>
      <spring:bind path="company.name"><c:if test="${status.error}">
        companyForm.data('bootstrapValidator').validateField("companyName")
      </c:if></spring:bind>
      <spring:bind path="company.address"><c:if test="${status.error}">
        companyForm.data('bootstrapValidator').validateField("companyAddress")
      </c:if></spring:bind>
      <spring:bind path="company.email"><c:if test="${status.error}">
        companyForm.data('bootstrapValidator').validateField("companyEmail")
      </c:if></spring:bind>
    }
  </script>
</body>
</html>

For each field, we check if it has failed validation, using the <spring:bind> tag and checking the status.error attribute. If the field has failed validation then we add the classes has-error and has-feedback to the form-group. This tells Bootstrap to render the field with error styling. We also add a form-control-feedback icon and place the error message in a help-block. On the icon and error message we add the class static-error to indicate that they were created by the server-side validation.

The markup we're creating here for fields in error is the same as that generated by the dynamic validation from BootstrapValidator. This ensures that server-side and client-side errors have the same look and feel.

If JavaScript is not enabled then the page will be rendered with the server-side validation errors shown.

If, however, JavaScript is enabled, then we can rely on BootstrapValidator to take over the validation. However, we need to ensure that it does not conflict with the static server-side error mark up.

To do this, in the onReady JavaScript function:

  • We remove the server-side error markup by removing all the elements marked as static-error. This prevents a conflict with the dynamically generated markup from BootstrapValidator.
  • We initialise BootstrapValidator on the company form, using a static JSON object defined in the company.js file (see below).
  • For each field that is in error, we trigger the validation to show the dynamic error message. This replaces the removed server-side validation with newly generated dynamic client-side validation.

This has the result of showing the same set of errors as the static page, but when the users interacts with the form the validation will be handled dynamically, switching to 'validation success' mark up when the validation succeeds.

Note: in the real world, the construction of the error handling markup is abstracted into a tag. I've shown it in full to make it more obvious.

Now, let's look at how we configure BootstrapValidator. I've pulled the configuration out into a static JavaScript file, company.js, since it doesn't change and can be cached.

var companyFormValidatorOptions = {
  message: 'This value is not valid',
  feedbackIcons: {
    valid: 'glyphicon glyphicon-ok',
    invalid: 'glyphicon glyphicon-remove',
    validating: 'glyphicon glyphicon-refresh'
  },
  fields: {
    companyName: {
      validators: {
        notEmpty: { message: 'The company name is required.'},
        stringLength: { max: 50, message: 'The company name can have at most 50 characters.'}
      }
    },
    companyAddress: {
      validators: {
        stringLength: { max: 255, message: 'The company address can have at most 255 characters.'}
      }
    },
    companyEmail: {
      validators: {
        emailAddress: { message: 'The email address is not valid.'},
        stringLength: { max: 255, message: 'The email address can have at most 255 characters.'}
      }
    }
  }
};

When we create this configuration, we ensure that the error messages are the same as from the JSR 303 annotations. In the future, I expect that we'll be able to generate this file from the annotations.

In this example, the client and server-side validation is equivalent. In some cases though, there is additional server-side validation, such as checking against existing values in the database. When this occurs, typically I've created a custom BootstrapValidator validator that looks for the specific value in the field and then shows the server-side error message. For example, if the server-side validation failed on the company name 'Black Pepper Software' because there was already a company with that name in the database, then the custom validator would trigger based on the specific value 'Black Pepper Software' and show the error. This enables the dynamic client-side validation to present the same error messages as the static server-side validation.

An alternative would be to use an AJAX validator that triggers the server-side validation dynamically.

Finally, let's review the core points that this example shows:

  • Server-side generated error markup should be the same style as the dynamically generated client-side markup to ensure a consistent user experience.
  • In JavaScript remove any server-side generated validation markup.
  • Make sure that client-side validation will re-create the server-side error messages.

In conclusion, with a little careful design, you can create your server-side validation and error handling to work for clients no matter whether they have JavaScript enabled or disabled.