Skip to main content

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​

AnnotationPurposeExample
@NotNullValue cannot be null@NotNull Long id
@NotBlankString cannot be null, empty, or only spaces@NotBlank String name
@NotEmptyCollection/String cannot be null or empty@NotEmpty List<String> items
@SizeChecks minimum/maximum size@Size(min=2, max=50)
@MinMinimum numeric value@Min(18)
@MaxMaximum numeric value@Max(60)
@PositiveNumber must be greater than 0@Positive
@PositiveOrZeroNumber must be 0 or greater@PositiveOrZero
@NegativeNumber must be less than 0@Negative
@NegativeOrZeroNumber must be 0 or less@NegativeOrZero
@EmailChecks email format@Email
@PatternChecks value against a regular expression@Pattern(regexp="...")
@PastDate must be in the past@Past
@PastOrPresentDate must be past or today@PastOrPresent
@FutureDate must be in the future@Future
@FutureOrPresentDate must be future or today@FutureOrPresent
@DigitsChecks number of integer/fraction digits@Digits(integer=5, fraction=2)
@DecimalMinMinimum decimal value@DecimalMin("10.5")
@DecimalMaxMaximum decimal value@DecimalMax("100.5")

@NotNull vs @NotBlank vs @NotEmpty​

Annotationnull 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 annotationSpring-specific annotation
Commonly used with request DTOsUseful for method-level validation
Supports nested validationSupports validation groups
Very common with @RequestBodyUseful 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();
}
}