Validation & Exception Handling
Spring Boot integrates with the Bean Validation API (JSR-380) via Hibernate Validator. Combined with a global @ControllerAdvice exception handler, you can produce consistent, informative error responses across your entire API.
1 Bean Validation Annotations
Java — Request DTO with Validation
import jakarta.validation.constraints.*;
import lombok.Data;
@Data
public class CreateUserRequest {
@NotBlank(message = "Name is required")
@Size(min = 2, max = 100, message = "Name must be 2-100 characters")
private String name;
@NotBlank(message = "Email is required")
@Email(message = "Must be a valid email address")
private String email;
@NotBlank(message = "Password is required")
@Size(min = 8, message = "Password must be at least 8 characters")
@Pattern(regexp = ".*[A-Z].*", message = "Password must contain at least one uppercase letter")
private String password;
@Min(value = 0, message = "Age must be positive")
@Max(value = 120, message = "Age must be realistic")
private Integer age;
@NotNull(message = "Role is required")
private Role role;
}
2 Global Exception Handler
Java — GlobalExceptionHandler.java
@RestControllerAdvice
public class GlobalExceptionHandler {
// Handle validation errors (400)
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidation(MethodArgumentNotValidException ex) {
Map<String, String> fieldErrors = new LinkedHashMap<>();
ex.getBindingResult().getFieldErrors().forEach(err ->
fieldErrors.put(err.getField(), err.getDefaultMessage())
);
return ResponseEntity.badRequest().body(
new ErrorResponse("VALIDATION_ERROR", "Input validation failed", fieldErrors)
);
}
// Handle not found (404)
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(new ErrorResponse("NOT_FOUND", ex.getMessage(), null));
}
// Handle conflict (409)
@ExceptionHandler(ConflictException.class)
public ResponseEntity<ErrorResponse> handleConflict(ConflictException ex) {
return ResponseEntity.status(HttpStatus.CONFLICT)
.body(new ErrorResponse("CONFLICT", ex.getMessage(), null));
}
// Catch-all (500)
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGeneral(Exception ex) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponse("INTERNAL_ERROR", "An unexpected error occurred", null));
}
}
// ErrorResponse DTO
@Data @AllArgsConstructor
public class ErrorResponse {
private String code;
private String message;
private Map<String, String> fieldErrors;
private String timestamp = Instant.now().toString();
}
3 Code Challenge
Challenge: Create a custom annotation
@UniqueEmail using ConstraintValidator that checks the database to ensure the email doesn't already exist during registration. Apply it to the CreateUserRequest.email field.