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.
1 2 3 4 5 6 7 |
<dependency> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-web</artifactid> </dependency> |
๐งฑ Example: Validating a User Registration Request
๐ Model: User.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
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<String> 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:
1 2 3 4 5 6 7 8 9 10 11 |
{ "timestamp": "2025-06-28T12:34:56.789+00:00", "status": 400, "errors": [ "Name is mandatory", "Email should be valid" ] } |
๐ค Sample JSON Input
โ Valid Input
1 2 3 4 5 6 7 8 |
{ "name": "John Doe", "age": 30, "email": "john@example.com" } |
โ Invalid Input
1 2 3 4 5 6 7 8 |
{ "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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
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<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); } } |
๐ Sample Error Response
1 2 3 4 5 6 7 |
{ "name": "Name is mandatory", "email": "Email should be valid" } |
๐ Validating Nested Objects
1 2 3 4 5 6 7 8 9 10 11 12 |
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
1 2 3 4 5 6 7 8 9 10 11 |
@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
1 2 3 4 5 6 7 8 9 10 11 |
public class UsernameValidator implements ConstraintValidator<ValidUsername, String> { 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:
1 2 3 4 5 |
@ValidUsername private String username; |
๐งช Testing with curl
1 2 3 4 5 6 |
curl -X POST http://localhost:8080/api/users \ -H "Content-Type: application/json" \ -d '{"name": "", "age": null, "email": "bad-email"}' |
Expected response:
1 2 3 4 5 6 7 8 |
{ "name": "Name is mandatory", "age": "Age is required", "email": "Email should be valid" } |