Validation error handling catches the exception that Spring throws when validation fails and turns it into a clear, structured response.It is used so the client gets “which field failed and why” instead of a long default Spring error or a server error. It is done with a @RestControllerAdvice class containing @ExceptionHandler methods, so one place handles errors for all controllers. Every error comes back in the same JSON shape, with a status, a message and an errors map of field: message.
Features
- One global handler (
GlobalExceptionHandler) for all controllers, so there is no try/catch in the controller. - Field-level messages, for example
"age": "Age must be at least 18". - The same response format for every error.
- Handles several kinds of failure, not only
@Valid(see the table below). - Correct HTTP status codes: 400, 404, 409 and 500.
- The generic 500 handler hides internal details from the client.
Validation Error Handling Types
| Failure | Exception caught | Status | Example |
|---|---|---|---|
Body validation (@Valid @RequestBody) |
MethodArgumentNotValidException |
400 | Name too short, bad email |
Parameter validation (@RequestParam, @PathVariable) |
ConstraintViolationException |
400 | /students/by-age?age=5 |
| Unreadable or wrong-type JSON | HttpMessageNotReadableException |
400 | "age": "twenty", broken JSON |
| Wrong type or missing value in the URL | MethodArgumentTypeMismatchException, MissingServletRequestParameterException |
400 | /students/abc, no age param |
| Business-rule error (custom exception) | DuplicateEmailException |
409 | Email already registered |
| Resource not found (custom exception) | StudentNotFoundException |
404 | /students/99 |
| Unexpected error | Exception |
500 | Any bug, with a safe message |
Hands-on Experiment: Creating a spring boot application to understand validation failures can be captured and presented as meaningful messages.
The following steps are involved in developing this hands-on experiment.
Step 1: Generate the Project
Step 2: Project Structure
Step 3: pom.xml
Step 4: application.properties
Step 5: Create Entity Class
Step 6: Create Repository Interface
Step 7: Create Service Class
Step 8: Create Controller Class
Step 9: Main Application Class
Step 10: Run the Main Application Class
Step 1: Configure the Project on Spring Initializr
Go to start.spring.io and set:
- Project: Maven
- Language: Java
- Spring Boot version: 3.2.x
- Group: com.example
- Artifact: validation
- Name: validation
- Package name: com.example.validation
- Packaging: Jar
- Java: 17
- Dependencies: Spring Web, Spring Data JPA, MySQL Driver, Thymeleaf, Validation I/O.
Click Generate to download the ZIP, then extract and import it into your IDE as a Maven project.
Step 2: Generated Project Structure

Step 3: Explore pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.5</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>student-error-handling-demo</artifactId>
<version>1.0.0</version>
<name>student-error-handling-demo</name>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Step 4: application.properties
spring.application.name=student-error-handling-demo
server.port=8082
spring.datasource.url=jdbc:mysql://localhost:3306/student_error_db?createDatabaseIfNotExist=true&useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
spring.datasource.username=root
spring.datasource.password=password
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.database-platform=org.hibernate.dialect.MySQLDialect
Code language: JavaScript (javascript)
Step 5: Create Entity Class
package com.example.errorhandling.entity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
@Entity
@Table(name = "students")
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank(message = "Name is required")
@Size(min = 3, max = 50, message = "Name must be between 3 and 50 characters")
private String name;
@NotBlank(message = "Email is required")
@Email(message = "Enter a valid email address")
@Column(unique = true)
private String email;
@NotNull(message = "Age is required")
@Min(value = 18, message = "Age must be at least 18")
@Max(value = 60, message = "Age must not be more than 60")
private Integer age;
@NotBlank(message = "Mobile number is required")
@Pattern(regexp = "^[6-9][0-9]{9}$",
message = "Mobile number must be 10 digits and start with 6, 7, 8 or 9")
@Column(length = 10)
private String mobile;
@NotBlank(message = "Course is required")
@Pattern(regexp = "Java|Spring Boot|SQL|React|Python",
message = "Course must be one of: Java, Spring Boot, SQL, React, Python")
private String course;
public Student() {
}
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public Integer getAge() { return age; }
public void setAge(Integer age) { this.age = age; }
public String getMobile() { return mobile; }
public void setMobile(String mobile) { this.mobile = mobile; }
public String getCourse() { return course; }
public void setCourse(String course) { this.course = course; }
}
Step 6: Create below Repository Interface
package com.example.errorhandling.repository;
import com.example.errorhandling.entity.Student;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface StudentRepository extends JpaRepository<Student, Long> {
boolean existsByEmail(String email);
List<Student> findByAge(Integer age);
}
Step 7: Create below Service class
package com.example.errorhandling.service;
import com.example.errorhandling.entity.Student;
import com.example.errorhandling.repository.StudentRepository;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class StudentService {
private final StudentRepository studentRepository;
public StudentService(StudentRepository studentRepository) {
this.studentRepository = studentRepository;
}
public Student register(Student student) {
if (studentRepository.existsByEmail(student.getEmail())) {
throw new DuplicateEmailException(student.getEmail());
}
student.setId(null); // a client must never choose the database id
return studentRepository.save(student);
}
public List<Student> getAll() {
return studentRepository.findAll();
}
public Student getById(Long id) {
return studentRepository.findById(id).orElseThrow(() -> new StudentNotFoundException(id));
}
public List<Student> getByAge(Integer age) {
return studentRepository.findByAge(age);
}
}
package com.example.errorhandling.service;
// Business-rule failure: the email is already used by another student
public class DuplicateEmailException extends RuntimeException {
private final String email;
public DuplicateEmailException(String email) {
super("Email already registered: " + email);
this.email = email;
}
public String getEmail() {
return email;
}
}
package com.example.errorhandling.service;
// Thrown when a student id does not exist
public class StudentNotFoundException extends RuntimeException {
public StudentNotFoundException(Long id) {
super("Student not found with id " + id);
}
}
Step 8: Create Controller class
package com.example.errorhandling.controller;
import com.example.errorhandling.entity.Student;
import com.example.errorhandling.service.StudentService;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
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.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("/students")
@Validated // needed so rules on @RequestParam / @PathVariable are checked too
public class StudentController {
private final StudentService studentService;
public StudentController(StudentService studentService) {
this.studentService = studentService;
}
// POST /students (body is validated; failures go to GlobalExceptionHandler)
@PostMapping
public ResponseEntity<Student> register(@Valid @RequestBody Student student) {
return ResponseEntity.status(HttpStatus.CREATED).body(studentService.register(student));
}
// GET /students
@GetMapping
public List<Student> getAll() {
return studentService.getAll();
}
// GET /students/by-age?age=22 (request parameter is validated)
@GetMapping("/by-age")
public List<Student> getByAge(
@RequestParam
@Min(value = 18, message = "Age must be at least 18")
@Max(value = 60, message = "Age must not be more than 60") Integer age) {
return studentService.getByAge(age);
}
// GET /students/1
@GetMapping("/{id}")
public Student getById(@PathVariable Long id) {
return studentService.getById(id);
}
}
package com.example.errorhandling.controller;
import com.example.errorhandling.service.DuplicateEmailException;
import com.example.errorhandling.service.StudentNotFoundException;
import com.fasterxml.jackson.databind.exc.InvalidFormatException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import java.time.LocalDateTime;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.TreeMap;
// Catches exceptions from every controller and turns them into one consistent JSON shape:
// { timestamp, status, error, message, path, errors: { field: message } }
@RestControllerAdvice
public class GlobalExceptionHandler {
// 1. @Valid @RequestBody failed -> field-level errors
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, Object>> handleBodyValidation(MethodArgumentNotValidException ex,
HttpServletRequest request) {
Map<String, String> errors = new TreeMap<>();
ex.getBindingResult().getFieldErrors().forEach(fieldError ->
errors.merge(fieldError.getField(), fieldError.getDefaultMessage(),
(first, second) -> first + "; " + second));
return build(HttpStatus.BAD_REQUEST, "Validation Failed",
"Input validation failed for " + errors.size() + " field(s)", request, errors);
}
// 2. Rules on @RequestParam / @PathVariable failed
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<Map<String, Object>> handleParamValidation(ConstraintViolationException ex,
HttpServletRequest request) {
Map<String, String> errors = new TreeMap<>();
for (ConstraintViolation<?> violation : ex.getConstraintViolations()) {
String path = violation.getPropertyPath().toString(); // e.g. getByAge.age
String field = path.substring(path.lastIndexOf('.') + 1); // -> age
errors.merge(field, violation.getMessage(), (first, second) -> first + "; " + second);
}
return build(HttpStatus.BAD_REQUEST, "Validation Failed",
"Input validation failed for " + errors.size() + " field(s)", request, errors);
}
// 3. Broken JSON, or a value of the wrong type (for example "age": "abc")
@ExceptionHandler(HttpMessageNotReadableException.class)
public ResponseEntity<Map<String, Object>> handleUnreadableBody(HttpMessageNotReadableException ex,
HttpServletRequest request) {
Map<String, String> errors = new TreeMap<>();
String message = "Malformed JSON request";
if (ex.getCause() instanceof InvalidFormatException invalid && !invalid.getPath().isEmpty()) {
String field = invalid.getPath().get(invalid.getPath().size() - 1).getFieldName();
if (field != null) {
errors.put(field, "Invalid value '" + invalid.getValue()
+ "'. Expected type: " + invalid.getTargetType().getSimpleName());
message = "Invalid value for field '" + field + "'";
}
}
return build(HttpStatus.BAD_REQUEST, "Bad Request", message, request, errors);
}
// 4. URL value of the wrong type (for example /students/abc or ?age=abc)
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
public ResponseEntity<Map<String, Object>> handleTypeMismatch(MethodArgumentTypeMismatchException ex,
HttpServletRequest request) {
String type = ex.getRequiredType() == null ? "valid value" : ex.getRequiredType().getSimpleName();
Map<String, String> errors = new TreeMap<>();
errors.put(ex.getName(), "Invalid value '" + ex.getValue() + "'. Expected type: " + type);
return build(HttpStatus.BAD_REQUEST, "Bad Request",
"Invalid value for parameter '" + ex.getName() + "'", request, errors);
}
// 5. A required request parameter is missing
@ExceptionHandler(MissingServletRequestParameterException.class)
public ResponseEntity<Map<String, Object>> handleMissingParam(MissingServletRequestParameterException ex,
HttpServletRequest request) {
Map<String, String> errors = new TreeMap<>();
errors.put(ex.getParameterName(), "Parameter is required");
return build(HttpStatus.BAD_REQUEST, "Bad Request",
"Missing parameter '" + ex.getParameterName() + "'", request, errors);
}
// 6. Business rule: email already registered -> field-level error on "email"
@ExceptionHandler(DuplicateEmailException.class)
public ResponseEntity<Map<String, Object>> handleDuplicateEmail(DuplicateEmailException ex,
HttpServletRequest request) {
Map<String, String> errors = new TreeMap<>();
errors.put("email", ex.getEmail() + " is already registered");
return build(HttpStatus.CONFLICT, "Conflict", "Email already registered", request, errors);
}
// 7. Student id not found
@ExceptionHandler(StudentNotFoundException.class)
public ResponseEntity<Map<String, Object>> handleNotFound(StudentNotFoundException ex,
HttpServletRequest request) {
return build(HttpStatus.NOT_FOUND, "Not Found", ex.getMessage(), request, null);
}
// 8. Anything unexpected: a safe message, no internal details leaked
@ExceptionHandler(Exception.class)
public ResponseEntity<Map<String, Object>> handleOthers(Exception ex, HttpServletRequest request) {
return build(HttpStatus.INTERNAL_SERVER_ERROR, "Internal Server Error",
"Something went wrong. Please try again later.", request, null);
}
private ResponseEntity<Map<String, Object>> build(HttpStatus status, String error, String message,
HttpServletRequest request, Map<String, String> errors) {
Map<String, Object> body = new LinkedHashMap<>();
body.put("timestamp", LocalDateTime.now().toString());
body.put("status", status.value());
body.put("error", error);
body.put("message", message);
body.put("path", request.getRequestURI());
if (errors != null && !errors.isEmpty()) {
body.put("errors", errors);
}
return ResponseEntity.status(status).body(body);
}
}
Step 9: Main Application
package com.example.errorhandling;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class StudentErrorHandlingApplication {
public static void main(String[] args) {
SpringApplication.run(StudentErrorHandlingApplication.class, args);
}
}
Step 10: Run the StudentErrorHandlingApplication





