| π Articles | π€ My Profile |
Validation is the process of ensuring that the data provided by a user (via an API, form, or request) is correct, complete, and within the required rules before processing it further.
β¨ For example:
Spring Boot provides powerful validation support using Jakarta Bean Validation (JSR 380) (formerly Hibernate Validator).
Here are the most commonly used annotations youβll encounter π
@Null β Must be null@NotNull β Must not be null (but can be empty, e.g., "")@NotEmpty β Must not be null and must have at least 1 element/character (for strings, collections, arrays)@NotBlank β Must not be null and must contain at least one non-whitespace character (better for strings)π Example:
public class UserRequest {
@NotNull(message = "ID cannot be null")
private Long id;
@NotEmpty(message = "Username cannot be empty")
private String username;
@NotBlank(message = "Password cannot be blank")
private String password;
}
@Size(min, max) β Defines min & max length, restrict length of string, collection, or arrayπ Example:
@Size(min = 3, max = 20, message = "Username must be 3-20 chars")
private String username;
@Min(value) β Minimum allowed value (Must be greater than or equal to given number)@Max(value) β Maximum allowed value (Must be less than or equal to given number)@Positive / @PositiveOrZero β Must be positive / β₯0@Negative / @NegativeOrZero β Must be negative / β€0@Digits(integer, fraction) β Restrict number of digits in integer and fraction partsπ Example:
@Min(value = 18, message = "Age must be at least 18")
@Max(value = 60, message = "Age must be at most 60")
private int age;
@Digits(integer = 6, fraction = 2, message = "Amount format is invalid")
private BigDecimal amount;
@AssertTrue β Must be true@AssertFalse β Must be falseπ Example:
@AssertTrue(message = "Must accept terms & conditions")
private boolean accepted;
@Past β Must be a past date@PastOrPresent β Must be today or past@Future β Must be a future date@FutureOrPresent β Must be today or futureπ Example:
@Past(message = "DOB must be in the past")
private LocalDate dob;
@Future(message = "Booking date must be in the future")
private LocalDate bookingDate;
@Email β Valid email format@Pattern(regexp = "...") β Must match given regex patternπ Example:
@Email(message = "Invalid email")
private String email;
@Pattern(regexp = "^[0-9]{10}$", message = "Phone must be 10 digits")
private String phone;
When built-in ones are not enough, you can define your own rules.
π Example:
@StrongPassword
private String password;
(Here, @StrongPassword is a custom annotation validated via ConstraintValidator.)
| π·οΈ Annotation | π Meaning |
|---|---|
π« @Null |
Must be null |
β
@NotNull |
Must not be null |
π§Ύ @NotBlank |
Must not be blank |
βοΈ @NotEmpty |
Not empty |
π @Size |
Length/size restriction |
π’ @Min / @Max |
Number range |
β @Positive |
Positive value |
β @Negative |
Negative value |
π― @Digits |
Number format |
β
@AssertTrue |
Must be true |
β @AssertFalse |
Must be false |
β³ @Past |
Past date |
π
@Future |
Future date |
π§ @Email |
Valid email |
π€ @Pattern |
Regex check |
π Best Practice (Industry Standard):
@ControllerAdvice.There are multiple ways you can implement validation. Letβs go step by step.
If using Maven, add the following to your pom.xml:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
or if you are using Gradle, add the following to your build.gradle:
implementation 'org.springframework.boot:spring-boot-starter-validation'
You annotate your DTOs (Request classes) with validation constraints.
Example:
import jakarta.validation.constraints.*;
public class UserRequest {
@NotBlank(message = "Name cannot be blank")
private String name;
@Email(message = "Invalid email format")
private String email;
@Min(value = 18, message = "Age must be at least 18")
private int age;
// Getters and Setters
}
Controller
import org.springframework.web.bind.annotation.*;
import org.springframework.http.ResponseEntity;
import jakarta.validation.Valid;
@RestController
@RequestMapping("/users")
public class UserController {
@PostMapping
public ResponseEntity<String> createUser(@Valid @RequestBody UserRequest userRequest) {
return ResponseEntity.ok("User Created Successfully!");
}
}
π Here, @Valid ensures validation is applied. If validation fails, Spring throws MethodArgumentNotValidException.
@ControllerAdvice or @RestControllerAdviceTo make responses user-friendly:
import org.springframework.http.*;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.*;
import java.util.*;
@RestControllerAdvice
public class GlobalExceptionHandler {
@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 new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST);
}
}
π Now invalid input returns structured JSON errors.
Spring also supports validating method parameters & return values with @Validated.
import jakarta.validation.constraints.Min;
import org.springframework.validation.annotation.Validated;
import org.springframework.stereotype.Service;
@Service
@Validated
public class PaymentService {
public void processPayment(@Min(1) double amount) {
System.out.println("Processing payment: " + amount);
}
}
π If someone calls processPayment(0), Spring throws a validation error.
When built-in annotations like @NotBlank or @Email arenβt enough, you can create your own validation annotation + custom validator class.
π The annotation defines:
@Target β Where the annotation can be applied (field, method, parameter).@Retention β How long it should be retained (we use RUNTIME).@Constraint β Points to the validator class that implements the validation logic.import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.*;
@Constraint(validatedBy = StrongPasswordValidator.class)
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface StrongPassword {
String message() default "Password must contain at least 1 uppercase, 1 lowercase, 1 digit, and 1 special character";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
π This class must implement ConstraintValidator<AnnotationType, FieldType>.
initialize() β runs before validation (optional).isValid() β actual validation logic.import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
public class StrongPasswordValidator implements ConstraintValidator<StrongPassword, String> {
@Override
public void initialize(CustomValidation constraintAnnotation) {
// (Optional) Initialize validator if needed
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
return value != null && value.matches("^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&]).+$");
}
}
π Just use the annotation like a built-in validator.
public class RegisterRequest {
@StrongPassword
private String password;
}
β
Password strength β @StrongPassword
β
Confirm password match β @PasswordMatches
β
Unique email (DB check) β @UniqueEmail
β
PAN/Aadhar/SSN validation β @ValidPAN
β
Mobile number format β @ValidPhone
When you create a custom validation annotation, you usually see these three properties:
public @interface StrongPassword {
String message() default "Password must contain at least 1 uppercase, 1 lowercase, 1 digit, and 1 special character";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
Letβs break them down one by one π
String message() default "...";β What is it?
β Why do we need it?
β How to use it?
@StrongPassword(message = "Password must follow company policy")
private String password;
Here, instead of the default, it will show βPassword must follow company policyβ when validation fails.
Class<?>[] groups() default {};β What is it?
β Why do we need it?
Sometimes you want different validation rules in different scenarios. Example:
β How to use it?
public interface CreateGroup {}
public interface UpdateGroup {}
public class UserRequest {
@NotNull(groups = CreateGroup.class)
private String name;
@NotNull(groups = {CreateGroup.class, UpdateGroup.class})
private String email;
}
Then, in your controller/service:
@PostMapping("/create")
public ResponseEntity<?> create(@Validated(CreateGroup.class) @RequestBody UserRequest user) {
...
}
Here only validations in the CreateGroup will run.
Class<? extends Payload>[] payload() default {};β What is it?
β Why do we need it?
β
How to use it?
First, define your own Payload:
public class Severity {
public interface Info extends Payload {}
public interface Critical extends Payload {}
}
Then apply:
@StrongPassword(payload = {Severity.Critical.class})
private String password;
Now your validator (or custom error handler) can check the payload type and decide how to handle the error (e.g., log it differently or give different HTTP status).
| Attribute | Purpose | Typical Usage |
|---|---|---|
message() |
Default error message | @NotNull(message = "Name is required") |
groups() |
Conditional / grouped validations | @Validated(CreateGroup.class) |
payload() |
Extra metadata (severity, category, etc.) | Advanced / custom cases |
π So, youβll almost always use message(), sometimes groups(), and rarely payload() unless youβre building a very advanced system.
Validation groups let you apply different validation rules for different use cases on the same entity/model.
For example:
Marker interfaces represent validation groups.
package com.sahu.springboot.validation.group;
public interface BasicInfo {}
public interface AdvancedInfo {}
Apply different constraints for different groups.
package com.sahu.springboot.validation.model;
import com.sahu.springboot.validation.group.BasicInfo;
import com.sahu.springboot.validation.group.AdvancedInfo;
import jakarta.validation.constraints.NotNull;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class User {
@NotNull(message = "Username is required", groups = {BasicInfo.class, AdvancedInfo.class})
private String username;
@NotNull(message = "Email is required for advanced info", groups = AdvancedInfo.class)
private String email;
}
We can validate the same User object differently depending on the endpoint.
package com.sahu.springboot.validation.controller;
import com.sahu.springboot.validation.group.BasicInfo;
import com.sahu.springboot.validation.group.AdvancedInfo;
import com.sahu.springboot.validation.model.User;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/user")
public class UserController {
//For Form based applications
@PostMapping("/registerBasic")
public String registerBasicInfo(@Validated(BasicInfo.class) @RequestBody User user,
BindingResult result) {
if (result.hasErrors()) {
return "β Basic Info Validation Failed: " + result.getAllErrors();
}
return "β
Basic Info Registered Successfully!";
}
@PostMapping("/registerAdvanced")
public String registerAdvancedInfo(@Validated(AdvancedInfo.class) @RequestBody User user) {
return "β
Advanced Info Registered Successfully!";
}
}
For REST APIs β usually donβt use BindingResult, instead let Spring throw MethodArgumentNotValidException and handle it globally in @ControllerAdvice/ @RestControllerAdvice.
For Form-based MVC apps (Thymeleaf, JSP, etc.) β BindingResult is useful because you want to redisplay the same form with validation messages.
π Key Points
User) can have different rules per use case.π₯οΈ Backend
π API Documentation
π’οΈ Database
π οΈ Build & Dependency Management
βοΈ Utilities
@Getter, @Setter, @Builder, etc.)spring-boot-validation
βββ π src/
β βββ π main/
β βββ π java/
β β βββ π com/sahu/springboot/basics/
β β βββ π config/
β β β βββ π OpenApiConfig.java
β β β βββ π OpenApiProperties.java
β β β
β β βββ π constant/
β β β βββ π ApiStatus.java
β β β
β β βββ π controller/rest/
β β β βββ π rest/
β β β βββ π ProductRestController.java
β β β βββ π UserRestController.java
β β β
β β βββ π dto/
β β β βββ π ApiResponse.java
β β β βββ π ProductRequest.java
β β β βββ π ProductResponse.java
β β β βββ π UserRequest.java
β β β βββ π UserResponse.java
β β β
β β βββ π exception/
β β β βββ π GlobalExceptionHandler.java
β β β βββ π ProductAlreadyExistException.java
β β β βββ π ProductNotFoundException.java
β β β βββ π UserAlreadyExistException.java
β β β βββ π UserNotFoundException.java
β β β
β β βββ π model/
β β β βββ π Product.java
β β β βββ π User.java
β β β
β β βββ π repository/
β β β βββ π ProductRepository.java
β β β βββ π UserRepository.java
β β β
β β βββ π service/
β β β βββ π impl/
β β β β βββ π ProductServiceImpl.java
β β β β βββ π UserServiceImpl.java
β β β β
β β β βββ π util/
β β β β βββ π ProductUtil.java
β β β β βββ π UserUtil.java
β β β β
β β β βββ π ProductService.java
β β β βββ π UserService.java
β β β
β β βββ π validation/
β β β βββ π group/
β β β β βββ π CreateGroup.java
β β β β βββ π UpdateGroup.java
β β β β
β β β βββ π StrongPassword.java
β β β βββ π StrongPasswordValidator.java
β β β
β β βββ π SpringBootValidationApplication.java
β β
β βββ π resources/
β βββ π application.yml
β
βββ π docker-compose.yml
βββ π pom.xml
You can find the complete code repository for this project on GitHub:
1οΈβ£ π³ Using Docker Compose (for MySQL container)
docker-compose up -d
β
This starts MySQL in a container (-d = detached mode).
π Verify with:
docker ps
π DB is now available at localhost:3307.
π Credentials (username, password, DB name) are in docker-compose.yml.
2οΈβ£ π» Run Directly in IntelliJ IDEA
SpringBootValidationApplication.java3οΈβ£ β‘ Run with Maven Command (CLI)
βΆοΈ Run app:
mvn spring-boot:run
π Run app with debug mode:
mvn spring-boot:run -Dspring-boot.run.fork=false -Dmaven.surefire.debug
π¦ Build JAR and run:
mvn clean package -DskipTests
java -jar target/spring-boot-validation-0.0.1-SNAPSHOT.jar
For a detailed running and demonstration of the application walkthrough,
watch the following YouTube video:
Coming Soonβ¦