3.5 Request Validation & Hibernate Validator
← Back to Chapter 3: REST API & Web Layer
Never trust client input. Spring Boot integrates the Jakarta Bean Validation specification (implemented by Hibernate Validator) via spring-boot-starter-validation to enforce constraints declaratively before business code runs.
Bean Validation
| Annotation | Purpose | Example |
|---|---|---|
@NotNull | Value cannot be null | @NotNull Long id |
@NotBlank | String cannot be null, empty, or only spaces | @NotBlank String name |
@NotEmpty | Collection/String cannot be null or empty | @NotEmpty List<String> items |
@Size | Checks minimum/maximum size | @Size(min=2, max=50) |
@Min | Minimum numeric value | @Min(18) |
@Max | Maximum numeric value | @Max(60) |
@Positive | Number must be greater than 0 | @Positive |
@PositiveOrZero | Number must be 0 or greater | @PositiveOrZero |
@Negative | Number must be less than 0 | @Negative |
@NegativeOrZero | Number must be 0 or less | @NegativeOrZero |
@Email | Checks email format | @Email |
@Pattern | Checks value against a regular expression | @Pattern(regexp="...") |
@Past | Date must be in the past | @Past |
@PastOrPresent | Date must be past or today | @PastOrPresent |
@Future | Date must be in the future | @Future |
@FutureOrPresent | Date must be future or today | @FutureOrPresent |
@Digits | Checks number of integer/fraction digits | @Digits(integer=5, fraction=2) |
@DecimalMin | Minimum decimal value | @DecimalMin("10.5") |
@DecimalMax | Maximum decimal value | @DecimalMax("100.5") |
@NotNull vs @NotBlank vs @NotEmpty
| Annotation | null allowed? | "" allowed? | " " allowed? | Mainly used for |
|---|---|---|---|---|
@NotNull | ❌ | ✅ | ✅ | Any object |
@NotEmpty | ❌ | ❌ | ✅ | String, Collection, Map, Array |
@NotBlank | ❌ | ❌ | ❌ | String |
Core Validation Annotations
public record RegisterUserRequest(
@NotBlank(message = "Username cannot be empty")
@Size(min = 3, max = 30, message = "Username must be between 3 and 30 characters")
String username,
@NotBlank(message = "Email is required")
@Email(message = "Email must be a valid email format")
String email,
@NotBlank(message = "Password cannot be blank")
@Pattern(
regexp = "^(?=.*[A-Za-z])(?=.*\\d)(?=.*[@$!%*#?&])[A-Za-z\\d@$!%*#?&]{8,}$",
message = "Password must be at least 8 characters with 1 letter, 1 number, and 1 special char"
)
String password,
@Min(value = 18, message = "User must be at least 18 years old")
@Max(value = 120, message = "Invalid age specified")
int age,
@NotNull(message = "Profile details required")
@Valid // Cascades validation into nested object!
UserProfileDTO profile
) {}
What is @Valid
Creating validation rules isn't enough.
You need to tell Spring:
Please execute the validation rules on this object.
That's what @Valid does.
@PostMapping("/users")
public User createUser(
@Valid @RequestBody UserRequest request) {
return userService.createUser(request);
}
Flow:
JSON Request
↓
@RequestBody
↓
UserRequest object
↓
@Valid
↓
Validation
↙ ↘
Valid Invalid
↓ ↓
Service Error
What is @Validated
@Valid is commonly used for validating request objects.
@Validated is a Spring-specific annotation that also supports validation groups and is commonly used for method-level validation.
Example:
@Service
@Validated
public class UserService {
public void getUser(@Min(1) Long userId) {
// Business logic
}
}
Here, Spring can validate the method parameter.
@Valid vs @Validated
@Valid | @Validated |
|---|---|
| Standard Jakarta validation annotation | Spring-specific annotation |
| Commonly used with request DTOs | Useful for method-level validation |
| Supports nested validation | Supports validation groups |
Very common with @RequestBody | Useful when more advanced validation is required |
Triggering Validation in the Controller
To activate validation, annotate the controller argument with @Valid (standard Jakarta) or @Validated (Spring variant):
@PostMapping("/register")
public ResponseEntity<UserResponse> register(@Valid @RequestBody RegisterUserRequest req) {
// If validation fails, MethodArgumentNotValidException is thrown BEFORE reaching here!
return ResponseEntity.ok(userService.register(req));
}
Custom Constraint Validator
When built-in annotations are insufficient, create custom validators:
1. Define the Annotation
@Target({ ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PhoneNumberValidator.class)
@Documented
public @interface ValidPhoneNumber {
String message() default "Invalid international phone number format";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
2. Implement the ConstraintValidator
public class PhoneNumberValidator implements ConstraintValidator<ValidPhoneNumber, String> {
private static final Pattern PHONE_REGEX = Pattern.compile("^\\+[1-9]\\d{1,14}$"); // E.164
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null) return true; // Let @NotNull handle null checks if required
return PHONE_REGEX.matcher(value).matches();
}
}