Data validation is a key part of building robust APIs. Spring Boot integrates with Jakarta Bean Validation (formerly JSR 380, previously known as Hibernate Validator) to provide a powerful and declarative validation mechanism.
This post will walk you through how to use @Valid along with validation annotations to ensure your incoming data meets the expected constraints.

โ Basic Setup
Step 1: Add Spring Web and Validation Dependencies
If you’re using Maven, just add spring-boot-starter-web. It already includes validation dependencies.
org.springframework.boot
spring-boot-starter-web
๐งฑ Example: Validating a User Registration Request
๐ Model: User.java
package com.kscodes.springboot.validationdemo.model;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
public class User {
@NotBlank(message = "Name is mandatory")
@Size(min = 2, max = 30, message = "Name must be between 2 to 30 characters")
private String name;
@NotNull(message = "Age is required")
private Integer age;
@Email(message = "Email should be valid")
private String email;
// Getters and Setters
}
๐ Controller: UserController.java
package com.kscodes.springboot.validationdemo.controller;
import com.kscodes.springboot.validationdemo.model.User;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import jakarta.validation.Valid;
@RestController
@RequestMapping("/api/users")
public class UserController {
@PostMapping
public ResponseEntity createUser(@Valid @RequestBody User user) {
// If validation passes, this line executes
return ResponseEntity.ok("User created: " + user.getName());
}
}
๐ฅ What happens if the input is invalid?
Spring will automatically return a 400 Bad Request with a descriptive error message like:
{
"timestamp": "2025-06-28T12:34:56.789+00:00",
"status": 400,
"errors": [
"Name is mandatory",
"Email should be valid"
]
}
๐ค Sample JSON Input
โ Valid Input
{
"name": "John Doe",
"age": 30,
"email": "john@example.com"
}
โ Invalid Input
{
"name": "",
"age": null,
"email": "not-an-email"
}
๐งฐ Common Jakarta Validation Annotations
| Annotation | Description |
|---|---|
@NotNull | Field must not be null |
@NotBlank | Not null and trimmed length > 0 |
@Size(min, max) | Checks size of string, list, etc. |
@Min, @Max | Checks numerical values |
@Email | Validates email format |
@Pattern(regexp = "") | Validates against regex |
@Positive, @Negative | Checks for positive/negative numbers |
๐ฏ Custom Error Handling with @ControllerAdvice
๐ Create GlobalExceptionHandler.java
package com.kscodes.springboot.validationdemo.exception;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ControllerAdvice;
import java.util.HashMap;
import java.util.Map;
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity
๐ Sample Error Response
{
"name": "Name is mandatory",
"email": "Email should be valid"
}
๐ Validating Nested Objects
public class User {
@Valid
private Address address;
}
public class Address {
@NotBlank
private String city;
}
Use @Valid on nested fields as well to trigger recursive validation.
โ๏ธ Custom Validation Annotation (Advanced)
Step 1: Define Annotation
@Constraint(validatedBy = UsernameValidator.class)
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidUsername {
String message() default "Invalid username";
Class>?>[] groups() default {};
Class extends Payload>[] payload() default {};
}
Step 2: Implement the Validator
public class UsernameValidator implements ConstraintValidator {
public void initialize(ValidUsername constraint) {}
public boolean isValid(String username, ConstraintValidatorContext context) {
return username != null && username.matches("[a-zA-Z0-9_]+");
}
}
Use it in your model:
@ValidUsername
private String username;
๐งช Testing with curl
curl -X POST http://localhost:8080/api/users \
-H "Content-Type: application/json" \
-d '{"name": "", "age": null, "email": "bad-email"}'
Expected response:
{
"name": "Name is mandatory",
"age": "Age is required",
"email": "Email should be valid"
}