Skip to main content

Spring MVC Validation

Introduction

Form validation is a critical part of any web application. It ensures that user input meets expected criteria before processing it, preventing bad data from entering your system and providing helpful feedback to users. Spring MVC offers robust validation support through the Bean Validation API (JSR-380) and its own validation framework.

In this tutorial, we'll learn how to implement validation in Spring MVC applications to ensure data integrity and enhance user experience.

Understanding Spring MVC Validation

Spring MVC validation works by applying constraints to model properties and then validating those constraints when form data is submitted. If validation fails, error messages are returned to the user.

The validation process in Spring MVC typically involves:

  1. Defining constraints: Annotating model properties with validation constraints
  2. Triggering validation: Using the @Valid or @Validated annotation in controller methods
  3. Handling validation errors: Processing validation results and displaying error messages to users

Setting Up Validation Dependencies

To use validation in Spring MVC, you need to include the following dependencies in your project:

xml
<!-- For Maven -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>

For Gradle:

groovy
// For Gradle
implementation 'org.springframework.boot:spring-boot-starter-validation'

Basic Validation Example

Let's start with a simple example by creating a User class with validation constraints:

java
import javax.validation.constraints.*;

public class User {
@NotEmpty(message = "Username cannot be empty")
@Size(min = 4, max = 20, message = "Username must be between 4 and 20 characters")
private String username;

@Email(message = "Email should be valid")
@NotEmpty(message = "Email cannot be empty")
private String email;

@NotEmpty(message = "Password cannot be empty")
@Size(min = 8, message = "Password must be at least 8 characters long")
private String password;

@Min(value = 18, message = "Age should not be less than 18")
private int age;

// Getters and setters
public String getUsername() {
return username;
}

public void setUsername(String username) {
this.username = username;
}

public String getEmail() {
return email;
}

public void setEmail(String email) {
this.email = email;
}

public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}
}

Now, let's create a controller to handle user registration:

java
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import javax.validation.Valid;

@Controller
public class UserController {

@GetMapping("/register")
public String showRegistrationForm(Model model) {
model.addAttribute("user", new User());
return "registration";
}

@PostMapping("/register")
public String registerUser(@Valid @ModelAttribute("user") User user,
BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return "registration";
}

// Process the user registration
return "redirect:/registration-success";
}
}

Creating the Registration Form

Create a Thymeleaf template for the registration form:

html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>User Registration</title>
<style>
.error {
color: red;
font-size: 0.8em;
}
</style>
</head>
<body>
<h1>User Registration</h1>
<form th:action="@{/register}" th:object="${user}" method="post">
<div>
<label>Username:</label>
<input type="text" th:field="*{username}" />
<span th:if="${#fields.hasErrors('username')}" th:errors="*{username}" class="error"></span>
</div>
<div>
<label>Email:</label>
<input type="text" th:field="*{email}" />
<span th:if="${#fields.hasErrors('email')}" th:errors="*{email}" class="error"></span>
</div>
<div>
<label>Password:</label>
<input type="password" th:field="*{password}" />
<span th:if="${#fields.hasErrors('password')}" th:errors="*{password}" class="error"></span>
</div>
<div>
<label>Age:</label>
<input type="number" th:field="*{age}" />
<span th:if="${#fields.hasErrors('age')}" th:errors="*{age}" class="error"></span>
</div>
<button type="submit">Register</button>
</form>
</body>
</html>

Common Validation Annotations

Spring MVC supports various validation constraints through the Bean Validation API:

AnnotationDescription
@NotNullValidates that the annotated value is not null
@NotEmptyValidates that the property is not null or empty (for String, Collection, Map, or Array)
@NotBlankValidates that the string is not null and contains at least one non-whitespace character
@MinValidates that the annotated value is not less than the specified minimum
@MaxValidates that the annotated value is not greater than the specified maximum
@SizeValidates that the annotated element size is between min and max
@EmailValidates that the annotated string is a well-formed email address
@PatternValidates that the annotated string matches the regular expression
@PastValidates that the date is in the past
@FutureValidates that the date is in the future

Custom Validation

Sometimes built-in constraints aren't sufficient. Let's create a custom validation annotation to ensure passwords match certain criteria:

  1. First, create the annotation:
java
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;

@Documented
@Constraint(validatedBy = StrongPasswordValidator.class)
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface StrongPassword {
String message() default "Password must contain at least one uppercase letter, one lowercase letter, one digit, and one special character";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
  1. Then, implement the validator:
java
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.regex.Pattern;

public class StrongPasswordValidator implements ConstraintValidator<StrongPassword, String> {
private static final String PASSWORD_PATTERN =
"^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]{8,}$";

@Override
public void initialize(StrongPassword constraintAnnotation) {
}

@Override
public boolean isValid(String password, ConstraintValidatorContext context) {
if (password == null) {
return false;
}
return Pattern.compile(PASSWORD_PATTERN).matcher(password).matches();
}
}
  1. Update the User class to use the custom annotation:
java
@NotEmpty(message = "Password cannot be empty")
@Size(min = 8, message = "Password must be at least 8 characters long")
@StrongPassword
private String password;

Group Validation

Sometimes, you might want to apply different sets of validations based on the context. Spring supports validation groups for this purpose:

  1. Define validation groups:
java
public interface ValidationGroups {
interface Registration {}
interface Update {}
}
  1. Specify groups in validation annotations:
java
public class User {
@NotEmpty(message = "Username cannot be empty", groups = {ValidationGroups.Registration.class, ValidationGroups.Update.class})
private String username;

@Email(message = "Email should be valid", groups = {ValidationGroups.Registration.class, ValidationGroups.Update.class})
private String email;

@NotEmpty(message = "Password cannot be empty", groups = ValidationGroups.Registration.class)
@StrongPassword(groups = ValidationGroups.Registration.class)
private String password;

// Getters and setters
}
  1. Update controller to use validation groups:
java
@PostMapping("/register")
public String registerUser(@Validated(ValidationGroups.Registration.class) @ModelAttribute("user") User user,
BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return "registration";
}

// Process the user registration
return "redirect:/registration-success";
}

Cross-Field Validation

Sometimes you need to validate properties against each other. Let's create a validator to ensure that a password confirmation matches the original password:

  1. Create a class-level validation annotation:
java
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PasswordMatchesValidator.class)
@Documented
public @interface PasswordMatches {
String message() default "Passwords don't match";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
  1. Implement the validator:
java
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class PasswordMatchesValidator
implements ConstraintValidator<PasswordMatches, UserRegistration> {

@Override
public void initialize(PasswordMatches constraintAnnotation) {
}

@Override
public boolean isValid(UserRegistration user, ConstraintValidatorContext context) {
boolean isValid = user.getPassword().equals(user.getConfirmPassword());

if (!isValid) {
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate())
.addPropertyNode("confirmPassword")
.addConstraintViolation();
}

return isValid;
}
}
  1. Create a user registration form class:
java
@PasswordMatches
public class UserRegistration {
@NotEmpty
@Size(min = 4, max = 20)
private String username;

@Email
@NotEmpty
private String email;

@NotEmpty
@StrongPassword
private String password;

@NotEmpty
private String confirmPassword;

// Getters and setters
}

Handling Validation Errors Programmatically

You can also manually add errors to the BindingResult:

java
@PostMapping("/register")
public String registerUser(@Valid @ModelAttribute("user") User user,
BindingResult bindingResult) {
// Custom validation logic
if (userService.isUsernameTaken(user.getUsername())) {
bindingResult.rejectValue("username", "error.user", "Username is already taken");
}

if (bindingResult.hasErrors()) {
return "registration";
}

// Process the user registration
return "redirect:/registration-success";
}

REST API Validation

For REST APIs, you can use the same validation annotations, and Spring will automatically convert validation errors into appropriate HTTP responses:

java
@RestController
@RequestMapping("/api/users")
public class UserRestController {

@PostMapping
public ResponseEntity<?> createUser(@Valid @RequestBody User user,
BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
Map<String, String> errors = new HashMap<>();
bindingResult.getFieldErrors().forEach(error ->
errors.put(error.getField(), error.getDefaultMessage())
);
return ResponseEntity.badRequest().body(errors);
}

// Process the user creation
return ResponseEntity.status(HttpStatus.CREATED).body(user);
}
}

Global Validation Error Handling

For APIs, you can create a global exception handler to handle validation errors consistently:

java
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

import java.util.HashMap;
import java.util.Map;

@ControllerAdvice
public class ValidationExceptionHandler {

@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Object> handleValidationExceptions(MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();

ex.getBindingResult().getAllErrors().forEach((error) -> {
String fieldName = ((FieldError) error).getField();
String errorMessage = error.getDefaultMessage();
errors.put(fieldName, errorMessage);
});

return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST);
}
}

Internationalization of Validation Messages

To support multiple languages for validation messages:

  1. Create message properties files:

messages.properties (default):

user.name.notempty=Username cannot be empty
user.email.notempty=Email cannot be empty
user.email.invalid=Please provide a valid email

messages_es.properties (Spanish):

user.name.notempty=El nombre de usuario no puede estar vacío
user.email.notempty=El correo electrónico no puede estar vacío
user.email.invalid=Por favor proporciona un correo electrónico válido
  1. Configure the message source:
java
@Configuration
public class MessageConfig {

@Bean
public MessageSource messageSource() {
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
messageSource.setBasename("classpath:messages");
messageSource.setDefaultEncoding("UTF-8");
return messageSource;
}

@Bean
public LocalValidatorFactoryBean getValidator() {
LocalValidatorFactoryBean bean = new LocalValidatorFactoryBean();
bean.setValidationMessageSource(messageSource());
return bean;
}
}
  1. Use message keys in validation annotations:
java
@NotEmpty(message = "{user.name.notempty}")
private String username;

@Email(message = "{user.email.invalid}")
@NotEmpty(message = "{user.email.notempty}")
private String email;

Best Practices for Validation

  1. Layer your validation: Implement validation at different layers of your application
  2. Use clear error messages: Help users understand what went wrong and how to fix it
  3. Validate early: Catch validation errors as early as possible
  4. Don't trust client-side validation: Always validate on the server side
  5. Use appropriate constraints: Choose the most specific constraint for your validation needs
  6. Group validations logically: Use validation groups for different contexts
  7. Separate validation from business logic: Keep your validation concerns separate from core business logic

Summary

Spring MVC's validation framework provides a powerful way to ensure data integrity in your web applications. By using Bean Validation annotations, custom validators, and properly handling validation errors, you can create robust and user-friendly forms that provide clear feedback when validation fails.

In this tutorial, we've covered:

  • Basic validation using standard constraints
  • Custom validations for specific requirements
  • Cross-field validations for related properties
  • Validation groups for contextual validation
  • Programmatically handling validation errors
  • REST API validation techniques
  • Internationalization of validation messages

With these techniques, you can implement comprehensive validation for any Spring MVC application.

Additional Resources

Exercises

  1. Create a registration form with the following validations:

    • Username: 5-20 characters, alphanumeric only
    • Password: At least 8 characters with at least one uppercase, lowercase, digit, and special character
    • Email: Valid email format
    • Age: Between 18 and 120
  2. Implement custom validation to check if a username is available in a database

  3. Add validation to ensure that a user's birthday is in the past and that they are at least 18 years old

  4. Create a form with validation groups for different user roles (admin vs. regular user)

  5. Implement a RESTful API with validation and proper error responses



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)