🛡️ Validation & Errors in Spring: The Bouncer at Your Data Club
Imagine you’re throwing a super fancy party. You hire a bouncer to make sure only guests with proper invitations get in. That bouncer? That’s validation in Spring!
🎯 What You’ll Learn
- Validation Fundamentals – How Spring checks if data is “good enough” to enter
- Custom Validators – Creating your own special rules
- Exception Handling – What happens when bad data tries to sneak in
🚪 Part 1: Validation Fundamentals
The Story of the Picky Restaurant
Think of a fancy restaurant that only accepts reservations with:
- A name (can’t be empty!)
- A phone number (must be real!)
- Number of guests (between 1 and 20)
Spring validation works the same way! Before data enters your app, Spring checks if it follows the rules.
Meet the Annotation Guards 🏷️
Spring uses special annotations (little tags) to set rules:
public class Reservation {
@NotBlank(message = "Name is required!")
private String guestName;
@Email(message = "Please use a real email!")
private String email;
@Min(value = 1, message = "At least 1 guest!")
@Max(value = 20, message = "Max 20 guests!")
private int numberOfGuests;
}
🎨 Common Validation Annotations
| Annotation | What It Checks | Example |
|---|---|---|
@NotNull |
Not null | “Something must exist here” |
@NotBlank |
Not empty, not just spaces | “Write something real!” |
@Size |
Length between min and max | Password: 8-20 characters |
@Email |
Valid email format | must have @ and domain |
@Min / @Max |
Number limits | Age: 0 to 150 |
@Pattern |
Matches a pattern | Phone: only digits |
How to Activate the Bouncer 🎬
In your controller, add @Valid before the data:
@PostMapping("/reserve")
public String makeReservation(
@Valid @RequestBody Reservation res,
BindingResult result) {
if (result.hasErrors()) {
// Uh oh! Something's wrong!
return "Please fix your info!";
}
// All good! Process the reservation
return "Table reserved!";
}
graph TD A["📥 User Sends Data"] --> B{🛡️ @Valid Check} B -->|✅ All Good| C["Process Request"] B -->|❌ Has Errors| D["BindingResult Catches Them"] D --> E["Return Error Messages"]
The BindingResult Detective 🔍
BindingResult is like a notepad that catches ALL the problems:
if (result.hasErrors()) {
for (FieldError error : result.getFieldErrors()) {
System.out.println(
error.getField() + ": " +
error.getDefaultMessage()
);
}
}
Output might be:
guestName: Name is required!
email: Please use a real email!
🔧 Part 2: Custom Validators
When Default Rules Aren’t Enough
Imagine you run a club that only allows people whose name starts with “VIP_”. The built-in rules can’t check that!
Time to create your own bouncer!
Step 1: Create the Annotation 🏷️
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = VIPValidator.class)
public @interface VIPName {
String message() default
"Name must start with VIP_";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload()
default {};
}
Step 2: Create the Validator Logic 🧠
public class VIPValidator
implements ConstraintValidator<VIPName, String> {
@Override
public boolean isValid(
String value,
ConstraintValidatorContext ctx) {
if (value == null) {
return false;
}
return value.startsWith("VIP_");
}
}
Step 3: Use It! 🚀
public class ClubMember {
@VIPName
private String memberName;
// Now only "VIP_John" works!
// "John" gets rejected!
}
graph TD A["📝 Create @Annotation"] --> B["🧠 Write Validator Class"] B --> C["🔗 Link with @Constraint"] C --> D["✨ Use on Fields!"]
Real Example: Age Checker 👶➡️👴
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = AdultValidator.class)
public @interface Adult {
String message() default
"Must be 18 or older!";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload()
default {};
}
public class AdultValidator
implements ConstraintValidator<Adult, Integer> {
@Override
public boolean isValid(
Integer age,
ConstraintValidatorContext ctx) {
return age != null && age >= 18;
}
}
🚨 Part 3: Exception Handling
When Things Go Wrong
Even with a bouncer, sometimes things break. Maybe:
- The database is down 💔
- Someone found a loophole 🕳️
- An unexpected error happened 💥
Spring has special ways to catch these problems!
The @ExceptionHandler Hero 🦸
Put this in your controller to catch specific errors:
@RestController
public class ReservationController {
@PostMapping("/reserve")
public String reserve(@Valid @RequestBody
Reservation res) {
// ... your logic
}
@ExceptionHandler(
MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>>
handleValidationErrors(
MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult()
.getFieldErrors()
.forEach(error ->
errors.put(
error.getField(),
error.getDefaultMessage()
));
return ResponseEntity
.badRequest()
.body(errors);
}
}
The @ControllerAdvice Guardian 🌍
Want to catch errors across ALL controllers? Use @ControllerAdvice:
@ControllerAdvice
public class GlobalErrorHandler {
@ExceptionHandler(
MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>>
handleValidation(
MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult()
.getFieldErrors()
.forEach(e ->
errors.put(
e.getField(),
e.getDefaultMessage()
));
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(errors);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<String> handleAll(
Exception ex) {
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("Oops! Something went wrong!");
}
}
graph TD A["❌ Error Occurs"] --> B{Where to Handle?} B -->|Single Controller| C["@ExceptionHandler"] B -->|All Controllers| D["@ControllerAdvice"] C --> E["📤 Return Nice Error"] D --> E
HTTP Status Codes You’ll Use 📊
| Code | Meaning | When to Use |
|---|---|---|
| 400 | Bad Request | Validation failed |
| 404 | Not Found | Item doesn’t exist |
| 500 | Server Error | Something broke! |
Custom Exception Example 🎯
// 1. Create your exception
public class ReservationNotFoundException
extends RuntimeException {
public ReservationNotFoundException(Long id) {
super("Reservation " + id + " not found!");
}
}
// 2. Handle it globally
@ControllerAdvice
public class GlobalHandler {
@ExceptionHandler(
ReservationNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public Map<String, String> handleNotFound(
ReservationNotFoundException ex) {
return Map.of("error", ex.getMessage());
}
}
// 3. Throw it when needed
@GetMapping("/reservation/{id}")
public Reservation get(@PathVariable Long id) {
return repo.findById(id)
.orElseThrow(() ->
new ReservationNotFoundException(id));
}
🎉 Putting It All Together
Here’s how validation and error handling work as a team:
graph TD A["📥 Request Arrives"] --> B{🛡️ Valid?} B -->|✅ Yes| C["Process Data"] B -->|❌ No| D["MethodArgumentNotValidException"] D --> E["@ExceptionHandler / @ControllerAdvice"] E --> F["📤 Send Error Response"] C -->|Error During Process| G["Other Exception"] G --> E
🌟 Key Takeaways
-
Validation = Your Bouncer 🚪
- Use
@Valid+ annotations to check incoming data BindingResultcollects all the problems
- Use
-
Custom Validators = Your Special Rules 🔧
- Create annotation + validator class
- Perfect for business-specific rules
-
Exception Handling = Your Safety Net 🥅
@ExceptionHandlerfor single controller@ControllerAdvicefor all controllers- Always return friendly error messages!
🚀 Quick Code Summary
// ✅ Validation
@Valid @RequestBody MyDto dto
// ✅ Custom Validator
@Constraint(validatedBy = MyValidator.class)
public @interface MyRule { }
// ✅ Exception Handler
@ExceptionHandler(MyException.class)
public ResponseEntity<?> handle(MyException e)
// ✅ Global Handler
@ControllerAdvice
public class GlobalHandler { }
Now you’re ready to guard your Spring app like a pro! 🎊
