๐ก๏ธ 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! ๐
