Learn to validate the request body of a REST API built with Spring MVC. Also, validate @PathVariable and @RequestParam parameters in resource URIs using the hibernate validator framework.
In this Spring MVC validation example, we will add validations in REST APIs created for the CRUD example.
1. Request Body Validation in Spring MVC
To use Hibernate Validator in a Spring MVC application (without Spring Boot), we need to manually configure the necessary validation components.
1.1. Maven dependency
Start with adding the necessary dependencies for Hibernate Validator and Spring MVC.
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>6.0.0</version> <!-- Adjust the version as per your setup -->
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>8.0.0.Final</version>
</dependency>
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>jakarta.el</artifactId>
<version>4.0.2</version> <!-- EL support for Hibernate Validator -->
</dependency>
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
<version>3.0.2</version> <!-- Java Bean Validation API -->
</dependency>
1.2. Enable Bean Validation Support
In Spring Boot, the bean validation is automatically enabled if any JSR-303 implementation (like hibernate validator 2.0) is available on the classpath.
But when not using Spring Boot, we need to add LocalValidatorFactoryBean.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
@Configuration
public class AppConfig {
@Bean
public LocalValidatorFactoryBean validator() {
return new LocalValidatorFactoryBean();
}
}
1.3. Add Hibernate Validator Annotations to Request Model
Add the bean validation annotations in model classes, which will store the request body data such as @NotEmpty
and @Email
.
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
public class UserDTO {
@NotNull(message = "Name cannot be null")
@Size(min = 2, max = 30, message = "Name must be between 2 and 30 characters")
private String name;
@NotNull(message = "Email cannot be null")
@Email(message = "Email should be valid")
private String email;
@NotNull(message = "Age cannot be null")
@Min(value = 18, message = "Age should not be less than 18")
private Integer age;
// Getters and Setters
}
1.4. Controller to Handle Requests with Validation
Use the @Valid
annotation in your controller to trigger validation. If validation fails, we can handle the exceptions as discussed in the next section.
import jakarta.validation.Valid;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
@Controller
public class UserController {
@PostMapping("/createUser")
public String createUser(@Valid @ModelAttribute("user") UserDTO user, BindingResult result, Model model) {
if (result.hasErrors()) {
return "userForm";
}
model.addAttribute("message", "User is valid!");
return "success";
}
}
1.5. Handle MethodArgumentNotValidException for Validation Errors
If validation fails, Spring will throw a MethodArgumentNotValidException. We can handle these errors by returning a meaningful JSON response using @ExceptionHandler.
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.servlet.ModelAndView;
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ModelAndView handleValidationException(MethodArgumentNotValidException ex) {
BindingResult result = ex.getBindingResult();
ModelAndView mav = new ModelAndView("userForm");
mav.addObject("user", result.getTarget());
mav.addObject("result", result);
return mav;
}
}
In this case, we are using the view-based request submission, and rather than using REST API to accept the incoming requests, we can handle validation exceptions globally using the ProblemDetail API.
import jakarta.validation.ConstraintViolationException;
import org.springframework.http.ProblemDetail;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.http.HttpStatus;
import java.util.HashMap;
import java.util.Map;
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ProblemDetail handleValidationExceptions(MethodArgumentNotValidException ex) {
ProblemDetail problemDetail = ProblemDetail.forStatus(HttpStatus.BAD_REQUEST);
problemDetail.setTitle("Validation failed");
problemDetail.setDetail("Input data validation failed");
BindingResult bindingResult = ex.getBindingResult();
Map<String, String> fieldErrors = new HashMap<>();
bindingResult.getFieldErrors().forEach(error -> fieldErrors.put(error.getField(), error.getDefaultMessage()));
problemDetail.setProperty("fieldErrors", fieldErrors);
return problemDetail;
}
}
1.6. Displaying Errors in View
On the view page, we can access the ‘result
‘ model (added in exception handler) and display the error messages.
<form action="createUser" method="post" modelAttribute="user">
Name: <input type="text" name="name" value="${user.name}" />
<c:if test="${result.hasFieldErrors('name')}">
<span class="error">${result.getFieldError('name').defaultMessage}</span>
</c:if>
<br/>
Email: <input type="email" name="email" value="${user.email}" />
<c:if test="${result.hasFieldErrors('email')}">
<span class="error">${result.getFieldError('email').defaultMessage}</span>
</c:if>
<br/>
Age: <input type="number" name="age" value="${user.age}" />
<c:if test="${result.hasFieldErrors('age')}">
<span class="error">${result.getFieldError('age').defaultMessage}</span>
</c:if>
<br/>
<input type="submit" value="Submit" />
</form>
2. Validating Query and Path Parameters
In Spring REST, parameters in request URI are captured via @PathVariable, and all query parameters are captured via @RequestParam.
To validate query parameters and path parameters in REST controllers, we can use annotations like @Valid
, @Validated
, and validation annotations from the Jakarta Validation API (@NotNull
, @Min
, etc.). Spring will automatically apply the validation logic to query and path parameters.
2.1. Enable Validation
Query and path parameter validation is not straightforward. We need to create MethodValidationPostProcessor, which will process the validation annotation explicitly.
@Configuration
public class AppConfig {
@Bean
public MethodValidationPostProcessor methodValidationPostProcessor() {
final MethodValidationPostProcessor methodValidationPostProcessor = new MethodValidationPostProcessor();
methodValidationPostProcessor.setValidator(validator());
return methodValidationPostProcessor;
}
@Bean
public LocalValidatorFactoryBean validator() {
return new LocalValidatorFactoryBean();
}
}
2.2. Add Validation Annotation to Path and Query Parameters
Now, we can apply validation for both query parameters and path parameters in the controller in the following manner:
- Use Jakarta validation annotations on parameters.
- Use @Validated annotation at the top of the class so it applies to all methods.
import jakarta.validation.Valid;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import org.springframework.stereotype.Controller;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
@Validated // Enables validation for method-level parameters
public class UserController {
// Validating Path Parameters directly
@GetMapping("/users/{id}")
@ResponseBody
public String getUserById(@PathVariable @Min(1) Long id) {
// Path variable validation, ensuring ID is a positive number
return "User ID: " + id;
}
// Validating Query Parameters directly
@GetMapping("/users/direct")
@ResponseBody
public String getUserDirectParams(@RequestParam @NotNull(message = "Name is required") String name,
@RequestParam @Min(value = 18, message = "Age should be at least 18") Integer age) {
return "User's name is: " + name + " and age is: " + age;
}
}
The exception handling part remains the same and can be reused.
3. FAQs
3.1. Difference between MethodValidationPostProcessor and LocalValidatorFactoryBean
It is very important to understand these classes and when to register which bean while adding validation to the Spring MVC application.
- LocalValidatorFactoryBean is the main bean used to set up bean validation in a Spring application context. It must be set up when we want to validate the fields inside a POJO, such as an API request or response body.
- MethodValidationPostProcessor is a BeanPostProcessor implementation that delegates to a JSR-303 provider for performing method-level validation on annotated methods. This enables method-level validation of method parameters and return values.
This is also very important to note that MethodValidationPostProcessor can be used without the LocalValidatorFactoryBean. Either we can use LocalValidatorFactoryBean alone, or we must use both beans together.
@Configuration
public class AppConfig {
@Bean
//Optional. Use when validating method parameters
public MethodValidationPostProcessor methodValidationPostProcessor() {
final MethodValidationPostProcessor methodValidationPostProcessor = new MethodValidationPostProcessor();
methodValidationPostProcessor.setValidator(validator());
return methodValidationPostProcessor;
}
@Bean
//Mandatory bean to enable the bean validation
public LocalValidatorFactoryBean validator() {
return new LocalValidatorFactoryBean();
}
}
3.2. Request parameter validation not working
The most common reason why validation of request parameters (query and path params) does not work is that we forgot to add the @Validated annotation at the top of the controller class.
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
@Service
@Validated // Enable method-level validation
public class UserService {
public void createUser(@NotNull @Size(min = 2) String username) {
// Business logic
}
}
3.3. MethodArgumentNotValidException vs ConstraintViolationException
MethodArgumentNotValidException
and ConstraintViolationException
are both exceptions related to validation in Spring, but they are used in different contexts.
- The MethodArgumentNotValidException is specifically thrown when bean validation fails for request body objects, typically when using @Valid in Spring MVC controllers. It works in conjunction with annotations like @Valid and @RequestBody.
- The ConstraintViolationException is thrown when constraint violations occur, often when performing method-level validation on JPA entities directly (e.g., when using Hibernate Validator for JPA entities or when validating method parameters in a service layer).
Happy Learning !!
Comments