Spring MVC Validation
📌 1. Purpose of Validation
- Ensure data integrity from client inputs.
- Enhance user experience by keeping user inputs and showing friendly error messages.
📌 2. Validation Flow (1) Type Conversion Stage
- During @ModelAttribute binding, type mismatches can occur.
- Without BindingResult → Spring throws a 400 Bad Request.
- With BindingResult → Controller is still called, and errors are stored in BindingResult.
(2) Validation Logic ✅ Recommended Practice:
- Use BindingResult with rejectValue() (for field errors), reject() (for global errors).
- Automatically adds FieldError and ObjectError.
if (!StringUtils.hasText(item.getItemName())) {
bindingResult.rejectValue("itemName", "required");
}
if (item.getPrice() != null && item.getQuantity() != null) {
int resultPrice = item.getPrice() * item.getQuantity();
if (resultPrice < 10000) {
bindingResult.reject("totalPriceMin", new Object[]{10000, resultPrice}, null);
}
}
📌 3. Using BindingResult
- For field errors:
- bindingResult.addError(new FieldError(String objectName, String field, @Nullable Object
rejectedValue, boolean bindingFailure, @Nullable String[] codes, @Nullable
Object[] arguments, @Nullable String defaultMessage))
==> It's a bit too tedious to manage.- instead, use -> bindingResult.rejectValue("fieldName", "errorCode");
- For global errors (object errors):
- bindingResult.addError(new ObjectError(String objectName, String defaultMessage) )
- instead, use -> bindingResult.reject("errorCode");
**When you use bindingResult.rejectValue(...), it internally creates a FieldError object and adds it to bindingResult.addError(...).
📌 4. Handling Messages
- Define messages in errors.properties
- Message Resolution Hierarchy:
- required.item.itemName (object + field)
- required.itemName (field only)
- required.java.lang.String (type-based)
- required (general fallback)
required.item.itemName=Item name is required.
required.itemName=Please enter a name.
required.java.lang.String=This field must be a string.
required=This field is required.
** If there is no message code and no default message, a message of the form ???errorCode??? is used.
📌 5. Validation Improvement Steps
Version Method Description
V1 | Manual if checks | Simple but repetitive |
V2 | FieldError, ObjectError | Supports input retention |
V3 | Message codes & errors.properties | i18n and reuse |
V4 | rejectValue(), reject() | Cleaner, automatic message lookup |
V5 | Extract Validator class | Separation of concerns |
V6 | @Validated + @InitBinder | Automatic validator binding |
📌 6. Custom Validator Separation
- Create class that implements Validator.
- Benefits:
- Reusable validation logic
- Keeps controllers clean
@Component
public class ItemValidator implements Validator {
@Override
public boolean supports(Class<?> clazz) {
return Item.class.isAssignableFrom(clazz);
}
@Override
public void validate(Object target, Errors errors) {
Item item = (Item) target;
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "itemName", "required");
// More logic...
}
}
📌 7. Using @Validated and @InitBinder
- Bind the validator to the controller using @InitBinder
- Automatically invoked when @Validated is used
**@Validated is an annotation that triggers (executes) the validation provided by Spring.
When you attach it with @ModelAttribute or @RequestBody in a controller, validation is automatically performed for that object.
** execution flow
- DispatcherServlet receives the HTTP request.
- It finds the appropriate controller and handler method using HandlerMapping.
- Then, it processes the method parameters using a set of HandlerMethodArgumentResolvers.
- If a parameter is annotated with @ModelAttribute (or omitted but still a model object), the ModelAttributeMethodProcessor handles it.
- Inside this processor, a WebDataBinder is created using WebDataBinderFactory.
- At the moment of WebDataBinder creation, if there is an @InitBinder method in the controller, it is invoked to register custom Validator(s).
- If the parameter is annotated with @Validated, the registered Validator’s supports() method is checked, and if matched, its validate() method is executed.
** @Validated and @InitBinder do not work with @RequestBody because @RequestBody uses HttpMessageConverter for binding, whereas @InitBinder and WebDataBinder only apply to @ModelAttribute-based form data binding.
**@requestBody???
--> For @RequestBody, @Valid or @Validated triggers JSR-303 Bean Validation via LocalValidatorFactoryBean, not using WebDataBinder or @InitBinder.
@InitBinder
public void init(WebDataBinder binder) {
binder.addValidators(itemValidator);
}
@PostMapping("/add")
public String addItem(@Validated @ModelAttribute Item item, BindingResult result) {
if (result.hasErrors()) {
return "form";
}
return "redirect:/success";
}