⌂ Home
Unit 24 · Forms & Validation

Spring MVC Forms & Validation — Layman Concepts

Understand how Spring form tags bind data to Java objects, what @Valid and BindingResult actually do, how to create custom validators, and how @ExceptionHandler and @ControllerAdvice handle errors globally — all in plain English.

Spring Form Taglib @ModelAttribute Binding @Valid & BindingResult Custom Validators @ExceptionHandler @ControllerAdvice Interview Q&A
JSP Form @ModelAttribute Object @Valid BindingResult Service / Error View

Spring MVC Form Taglib — Why Not Plain HTML Forms?

“Spring's form tags do magic that plain HTML cannot”

🧩 Smart Form Analogy

Regular HTML forms are “blind” — they just send raw text. Spring MVC form tags are “intelligent” — they know about your Java objects. They automatically populate form fields with existing Java object values (great for edit forms), and they automatically bind submitted values back to the Java object. They also know how to show validation error messages next to the right fields.

Plain HTML form (basic)

<form action="/save" method="post"> <input type="text" name="employeeName"/> <!-- No auto-population, no binding, no error display --> </form>

Spring MVC form tag (smart)

<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %> <form:form action="/save" method="post" modelAttribute="employee"> <form:input path="employeeName"/> <form:errors path="employeeName"/> <!-- Auto-populates from Employee object, shows validation errors --> </form:form>

🔑 The key attribute is modelAttribute. This tells the form which Java object in the Model to bind to. Every form field uses path to specify which field of that object to bind to. The attribute name must match what the controller puts in the Model.

@ModelAttribute — Automatic Form-to-Object Binding

“Spring fills your Java object from the form automatically”

🧲 Stamp Machine Analogy

Imagine a form with 10 fields. Without @ModelAttribute, you'd write request.getParameter("name") for each field, then manually set each value on the object. @ModelAttribute is like a stamp machine — it reads all form fields, matches them to object fields by name, and fills the entire object automatically in one step.

@ModelAttribute on method parameter

When used on a parameter in a controller method, Spring automatically creates the object, reads all HTTP request parameters, and sets matching fields (by name).

// Form field name="employeeName" → emp.setEmployeeName() @PostMapping("/save") public String save(@ModelAttribute Employee emp) { // emp is already populated from form fields! service.save(emp); return "success"; }

@ModelAttribute on a method (pre-populating model)

When used on a method in the controller class, that method runs BEFORE every handler method in the class. Used to pre-load common data like dropdown options.

@ModelAttribute("countries") public List<String> loadCountries() { return Arrays.asList("India", "USA", "UK"); // model.get("countries") is available in every view }
🚨 The modelAttribute and @ModelAttribute Connection

The modelAttribute="employee" on <form:form> must match the attribute name put in the Model by the controller for the GET request. If the controller does model.addAttribute("employee", new Employee()), then modelAttribute must be "employee". Case-sensitive!

Static UI Components — Text, Password, Textarea, Checkbox, Radio

“Form fields where the options don't change”

TagHTML EquivalentUse Case
<form:input path="name"/><input type="text">Text input fields
<form:password path="pwd"/><input type="password">Password field (never pre-filled)
<form:textarea path="address"/><textarea>Multi-line text
<form:checkbox path="agree"/><input type="checkbox">Single boolean checkbox
<form:radiobutton path="gender" value="M"/><input type="radio">Single radio button option
<form:errors path="name"/>No HTML equivalentDisplays validation error for the field

🔑 path attribute = Java object field name. Spring reads the field value to pre-fill the control (GET request), and writes the submitted value back to the field (POST request). This is two-way binding.

Dynamic UI Components — Select, Checkboxes, RadioButtons

“Form fields where the options come from a Java Collection”

📷 Dynamic vs Static Analogy

Static: You hardcode the options in JSP. Dynamic: The options come from a Java List, Map, or Array loaded by the controller (e.g., list of countries from a database). Spring's dynamic form tags automatically render all items in the collection as HTML options.

<!-- Controller: puts a list in model --> model.addAttribute("countries", Arrays.asList("India", "USA", "UK")); <!-- JSP: renders a dropdown from that list --> <form:select path="country" items="${countries}"/> <!-- Renders multiple checkboxes from a list --> <form:checkboxes path="hobbies" items="${hobbyList}"/> <!-- Renders multiple radio buttons from a list --> <form:radiobuttons path="gender" items="${genderOptions}"/>

Items attribute with a Map

When items is a Map, the Map KEY becomes the option VALUE sent to server, and the Map VALUE becomes the visible text shown to user. Perfect for coded dropdowns (e.g., "IN" → "India").

Map<String, String> countries = new HashMap<>(); countries.put("IN", "India"); // key=value sent, val=label shown countries.put("US", "United States");

checkboxes (plural) vs checkbox

<form:checkbox> = single checkbox bound to a boolean field.
<form:checkboxes> = multiple checkboxes generated from a collection, bound to a List field. Submitted values are collected into a List automatically.

Why Do We Need Server-Side Validation?

“Client-side validation is not enough”

👥 Border Security Analogy

Client-side validation (JavaScript in browser) is like airport security at departure. Important, but a determined person can bypass it (just disable JavaScript). Server-side validation is like immigration at the destination airport — it's the real checkpoint. Even if someone bypasses the browser check and sends a raw POST request with garbage data, server-side validation catches it.

Client-side only (Bad)

  • Can be bypassed by disabling JavaScript
  • Can be bypassed by sending raw HTTP requests (Postman, curl)
  • Cannot access database to check business rules

Server-side validation (Essential)

  • Cannot be bypassed — always runs on server
  • Can access database, session, application context
  • Can enforce complex business rules (e.g., username must be unique)

Bean Validation Annotations on Your Entity/DTO

“Declare validation rules directly on your Java fields”

@NotNull(message = "Name cannot be null") @NotEmpty(message = "Name cannot be empty") @Size(min = 2, max = 50, message = "Name must be 2-50 characters") private String employeeName; @Min(value = 18, message = "Age must be at least 18") @Max(value = 65, message = "Age must not exceed 65") private int age; @Email(message = "Must be a valid email") private String email; @Pattern(regexp = "[A-Z]{3}[0-9]{3}", message = "Must be like ABC123") private String employeeCode; @Past(message = "Joining date must be in the past") private Date joiningDate;
AnnotationWhat it checksApplies to
@NotNullValue is not nullAny type
@NotEmptyNot null AND not empty string/collectionString, Collection
@NotBlankNot null AND not whitespace-onlyString
@Size(min,max)Length within rangeString, Collection
@Min(value)Number ≥ valueint, long, etc.
@Max(value)Number ≤ valueint, long, etc.
@EmailValid email formatString
@Pattern(regexp)Matches regex patternString
@PastDate is in the pastDate, LocalDate
@FutureDate is in the futureDate, LocalDate
🚨 @NotEmpty vs @NotBlank vs @NotNull

@NotNull: Passes for "" (empty string). @NotEmpty: Fails for null AND "". @NotBlank: Fails for null, "", AND " " (spaces only). For form fields, @NotBlank is usually what you want for String fields.

@Valid and BindingResult — How Validation Is Triggered

“The two annotations that make validation actually work”

✅ Customs & Report Analogy

@Valid is like telling customs: “Please inspect this luggage.” The inspection (validation) runs. BindingResult is like the customs report — it collects all violations found. You then check the report: if there are violations, send the passenger back. If clean, let them through.

@PostMapping("/saveEmployee") public String saveEmployee( @Valid @ModelAttribute Employee employee, // @Valid triggers validation BindingResult result, // collects errors (must be immediately after!) Model model) { if (result.hasErrors()) { // Validation failed! Redisplay form with error messages return "employeeForm"; // <form:errors> in JSP will show errors } // Validation passed! Proceed with business logic service.saveEmployee(employee); return "success"; }
🚨 Critical Rule: BindingResult must be IMMEDIATELY after the @Valid object

If any other parameter appears between the @Valid @ModelAttribute parameter and BindingResult, Spring throws a BindException and the method never executes. Always keep them adjacent: @Valid Employee emp, BindingResult result.

What BindingResult provides

  • result.hasErrors() — true if any validation failed
  • result.getFieldErrors() — List of all field-level errors
  • result.getFieldError("fieldName") — Error for a specific field
  • result.getErrorCount() — Total number of errors

Showing errors in JSP

<form:input path="employeeName"/> <!-- Shows validation error for employeeName field --> <form:errors path="employeeName" cssClass="error-msg"/> <!-- Show all errors at once --> <form:errors path="*"/>

Custom Validation — Two Approaches

“When built-in annotations aren't enough”

Approach 1: Spring Validator Interface

Implement org.springframework.validation.Validator. Write validation logic in Java code. Register it in the controller and call it manually (or with @InitBinder).

@Component public class EmployeeValidator implements Validator { public boolean supports(Class<?> cls) { return Employee.class.equals(cls); } public void validate(Object target, Errors errors) { Employee emp = (Employee) target; if (emp.getSalary() < 10000) { errors.rejectValue("salary", "min.salary", "Salary must be at least 10000"); } } }

Approach 2: Custom Constraint Annotation

Create your own annotation (like @NotBlank or @Email) backed by a ConstraintValidator class. More reusable — you can apply it to any field in any class.

// 1. Create the annotation @Target({ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy = SalaryValidator.class) public @interface ValidSalary { String message() default "Invalid salary"; Class[] groups() default {}; Class[] payload() default {}; } // 2. Create the validator class public class SalaryValidator implements ConstraintValidator<ValidSalary, Double> { public boolean isValid(Double salary, ConstraintValidatorContext ctx) { return salary != null && salary >= 10000; } } // 3. Use it just like built-in annotations: @ValidSalary private Double salary;

@ExceptionHandler — Handling Errors in the Controller

“Catch exceptions and show friendly error pages instead of stack traces”

🥞 Safety Net Analogy

Without exception handling, when an error occurs the user sees an ugly Java stack trace. @ExceptionHandler is like a safety net below your controller. When any exception is thrown and not caught, it falls through to the @ExceptionHandler method, which gracefully shows a user-friendly error page.

@Controller public class EmployeeController { @GetMapping("/employee/{id}") public String getEmployee(@PathVariable int id, Model model) { Employee emp = service.findById(id); // may throw EmployeeNotFoundException model.addAttribute("employee", emp); return "employeeDetail"; } // This method catches EmployeeNotFoundException from ANY method in THIS controller @ExceptionHandler(EmployeeNotFoundException.class) public String handleNotFound(EmployeeNotFoundException ex, Model model) { model.addAttribute("message", ex.getMessage()); return "error404"; // show a friendly error page } }
🚨 Limitation of @ExceptionHandler inside a Controller

@ExceptionHandler inside a controller class only handles exceptions thrown by that controller. If the same exception occurs in a different controller, it won't be caught. That's why @ControllerAdvice was created — for global exception handling.

@ControllerAdvice — Global Exception Handling for All Controllers

“One class to handle exceptions from ALL controllers”

🏢 Company HR Analogy

@ExceptionHandler inside a controller = each department handles its own problems. @ControllerAdvice = the HR department that handles problems from ALL departments. Any unhandled exception from any controller flows to @ControllerAdvice methods. One place to define all error handling logic.

@ControllerAdvice // Applies to ALL controllers in the application public class GlobalExceptionHandler { // Handles EmployeeNotFoundException from any controller @ExceptionHandler(EmployeeNotFoundException.class) public ModelAndView handleNotFound(EmployeeNotFoundException ex) { ModelAndView mav = new ModelAndView("error/notFound"); mav.addObject("message", ex.getMessage()); return mav; } // Generic fallback handler for any other exception @ExceptionHandler(Exception.class) public ModelAndView handleGeneral(Exception ex) { ModelAndView mav = new ModelAndView("error/general"); mav.addObject("error", "An unexpected error occurred"); return mav; } }

What @ControllerAdvice can contain

  • @ExceptionHandler methods — Global exception handlers
  • @InitBinder methods — Custom data binding for all controllers
  • @ModelAttribute methods — Data added to every model in every controller

Scoping @ControllerAdvice

By default, @ControllerAdvice applies to ALL controllers. You can restrict it:

// Only for controllers in this package @ControllerAdvice("com.acc.controller") // Only for specific controllers @ControllerAdvice(assignableTypes = {EmpController.class})

🔑 Best practice: Always have one @ControllerAdvice with a generic @ExceptionHandler(Exception.class) as a last resort. This prevents users from ever seeing raw stack traces. More specific exception handlers are checked first, then the generic one is the fallback.

Top Interview Questions with Answers

“Questions about forms, validation, and exception handling”

Why use Spring MVC form tags instead of plain HTML form tags?
Spring form tags (form:form, form:input, etc.) support two-way data binding. They automatically populate form fields from a bound Java object (great for edit forms) and automatically bind submitted values back to the object. They also display field-specific validation error messages via form:errors. Plain HTML forms cannot do any of this automatically.
What is the modelAttribute attribute on form:form?
It specifies the name of the Model attribute (Java object) that the form is bound to. The path attributes on each form field refer to fields of this bound object. If you set modelAttribute="employee", Spring will pre-populate fields from model.getAttribute("employee") and bind submitted values back to it.
How does @Valid and BindingResult work?
@Valid on the @ModelAttribute parameter triggers Bean Validation (runs all @NotNull, @Size, etc. constraints on the object). BindingResult, placed immediately after the @Valid parameter, collects all validation errors. You check result.hasErrors() — if true, return the form view (errors are shown via form:errors). If false, proceed with business logic.
What happens if BindingResult is not placed immediately after the @Valid parameter?
Spring throws a BindException and the controller method never executes. BindingResult MUST be the very next parameter after the @Valid @ModelAttribute parameter. No other parameter can be between them.
What is the difference between @ExceptionHandler and @ControllerAdvice?
@ExceptionHandler in a controller handles exceptions only from THAT controller. @ControllerAdvice is a class-level annotation that applies @ExceptionHandler methods globally to ALL controllers. Use @ControllerAdvice for application-wide error handling to avoid repeating exception handling in every controller.
How do you create a custom validation annotation?
1) Create an annotation with @Constraint(validatedBy = YourValidator.class). 2) Create YourValidator implementing ConstraintValidator<YourAnnotation, FieldType> with an isValid() method. 3) Apply your annotation to any field. Spring auto-discovers and runs it when @Valid is triggered.
What is the difference between @NotNull, @NotEmpty, and @NotBlank?
@NotNull: fails only if value is null. @NotEmpty: fails if null OR empty string/collection. @NotBlank: fails if null, empty string, or whitespace-only string. For form text fields, @NotBlank is usually the right choice since users can submit spaces.
What does form:errors do?
form:errors displays validation error messages from BindingResult for a specific field (path="fieldName") or all fields (path="*"). When @Valid fails and you return to the form view, Spring automatically transfers the errors from BindingResult to the model, and form:errors reads them and renders the error messages next to the appropriate form fields.

⚡ Memory Cheat Sheet

“Last-minute revision before your interview”

form:form modelAttribute
Binds form to Java object
in Model
form:input path
Two-way bind to
Java field name
@ModelAttribute param
Auto-fill Java object
from form fields
@Valid
Triggers Bean Validation
on the next object
BindingResult
Collects errors
Must be AFTER @Valid object
@NotBlank
Best for String fields
Rejects spaces too
@ExceptionHandler
Per-controller only
exception catching
@ControllerAdvice
Global exception handler
all controllers
Custom annotation validator
Annotation + ConstraintValidator
isValid() method
↑ Back to top