Spring MVC Validation
Learn how to validate bound form data with @Valid and BindingResult, and how to display validation errors back to the user.
Introduction
A form-backing object gets populated whether the data is sensible or not - an empty name, a negative age, or a malformed email will all bind just fine. Spring MVC integrates with the Bean Validation standard (JSR 380) so you can declare validation rules directly on your form object and have Spring check them automatically. This lesson covers @Valid, BindingResult, and how to show validation errors back to the user.
- Why server-side validation is required even with client-side checks.
- Common Bean Validation annotations like @NotBlank, @Size, and @Min.
- How @Valid triggers validation on a bound object.
- How BindingResult captures validation errors without throwing an exception.
- How to display field-level errors in a Thymeleaf view.
Why Validate on the Server
Client-side validation (HTML required attributes, JavaScript checks) improves user experience but can always be bypassed - a user can disable JavaScript, or a request can be sent directly to your endpoint with a tool like curl. Server-side validation is the only validation you can actually rely on to protect your application's data integrity.
Bean Validation Annotations
The jakarta.validation.constraints package (included via the spring-boot-starter-validation dependency) provides annotations you place directly on the fields of your form object to declare validation rules.
| Annotation | Meaning |
|---|---|
| @NotBlank | String must not be null and must contain at least one non-whitespace character |
| @NotNull | Value must not be null (works on any type) |
| @Size(min=, max=) | String, collection, or array length must fall within the given range |
| @Min / @Max | Numeric value must be at least / at most the given value |
| String must be formatted like a valid email address |
public class UserForm {
@NotBlank(message = "Name is required") private String name;
@NotBlank(message = "Email is required") @Email(message = "Email must be a valid address") private String email;
@Min(value = 18, message = "Age must be at least 18") private int age;
// getters and setters}Triggering Validation with @Valid
Adding @Valid in front of a @ModelAttribute parameter tells Spring to run Bean Validation on the bound object before the method body executes.
@PostMapping("/create")public String submitForm(@Valid @ModelAttribute UserForm userForm, BindingResult bindingResult) { if (bindingResult.hasErrors()) { return "user-form"; // re-show the form with error messages } userService.register(userForm); return "redirect:/users";}BindingResult must be declared as the parameter immediately following the @Valid object. If anything else comes between them, Spring throws validation errors as an exception instead of letting you handle them gracefully.
Reading Errors with BindingResult
BindingResult holds the outcome of both data binding and validation for the preceding object. hasErrors() tells you whether anything went wrong, and getFieldErrors() lets you inspect exactly which fields failed and why.
if (bindingResult.hasErrors()) { for (FieldError error : bindingResult.getFieldErrors()) { System.out.println(error.getField() + ": " + error.getDefaultMessage()); } return "user-form";}Click Run to see what this code prints.
Displaying Errors in the View
Because BindingResult is automatically exposed to the view alongside the form object, template engines can render per-field errors without any extra wiring. In Thymeleaf, th:if="${#fields.hasErrors('name')}" and th:errors="*{name}" do exactly this.
<input type="text" th:field="*{name}" /><span th:if="${#fields.hasErrors('name')}" th:errors="*{name}" class="error"></span>Common Mistakes
- Forgetting @Valid entirely, so the annotations on the form object are never actually checked.
- Putting a parameter between the @Valid object and BindingResult, causing Spring to throw instead of populate it.
- Relying only on client-side validation and skipping @Valid on the server.
- Not returning the form view again when bindingResult.hasErrors() is true, which silently proceeds with invalid data.
- Forgetting to add spring-boot-starter-validation as a dependency, so the validation annotations have no effect.
Best Practices
- Always check bindingResult.hasErrors() immediately and return the form view if validation failed.
- Give every validation annotation a clear, user-facing message.
- Validate both format (like @Email) and business rules (like @Min(18)) at the field level whenever possible.
- Keep client-side validation as a UX nicety, never as the only line of defense.
- Group related constraints on a form object rather than duplicating checks in the controller.
Frequently Asked Questions
Spring throws a MethodArgumentNotValidException, which normally results in a 400 Bad Request response unless you handle it with an exception handler.
Yes - annotate the nested field with @Valid as well (for example @Valid private Address address;) so Bean Validation cascades into it.
Yes, by creating a custom annotation paired with a ConstraintValidator implementation, which lets you express rules that the built-in annotations do not cover.
Key Takeaways
- Server-side validation is required because client-side checks can always be bypassed.
- Bean Validation annotations like @NotBlank, @Size, and @Min declare rules directly on the form object.
- @Valid triggers those checks when a bound object is passed into a handler method.
- BindingResult, declared right after the @Valid parameter, captures errors instead of throwing an exception.
- Template engines can render field-level errors directly from BindingResult with no extra wiring.
Summary
Validation keeps bad data from ever reaching your service and persistence layers. But not every failure is a validation problem - sometimes a database call fails, or a resource simply does not exist. Next, you will learn how Spring MVC centralizes handling for exceptions like those.