Chapter 2: Data Validation
Introduction
Data validation is essential to ensure the integrity and quality of data in your application. Spring Boot integrates Bean Validation (JSR 380) to easily validate input data.
Bean Validation (JSR 380)
Maven Dependency
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
Note: This dependency is already included in spring-boot-starter-web.
Main Validation Annotations
Summary Table
| Annotation | Usage | Example |
|---|---|---|
@NotNull | Cannot be null | @NotNull private String name; |
@NotEmpty | Cannot be null or empty | @NotEmpty private String email; |
@NotBlank | Cannot be null, empty, or just spaces | @NotBlank private String title; |
@Size | Min/max size | @Size(min=3, max=50) private String name; |
@Min | Minimum value | @Min(0) private int age; |
@Max | Maximum value | @Max(100) private int grade; |
@Email | Valid email format | @Email private String email; |
@Pattern | Regular expression | @Pattern(regexp="[0-9]{10}") private String phone; |
@Past | Date in the past | @Past private LocalDate birthDate; |
@Future | Date in the future | @Future private LocalDate appointmentDate; |
Complete Example: Entity with Validation
package com.example.demo.model;
import jakarta.persistence.*;
import jakarta.validation.constraints.*;
@Entity
@Table(name = "products")
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank(message = "Name cannot be empty")
@Size(min = 3, max = 50, message = "Name must be between 3 and 50 characters")
private String name;
@NotNull(message = "Price is required")
@Min(value = 0, message = "Price cannot be negative")
private Double price;
@NotNull(message = "Quantity is required")
@Min(value = 0, message = "Quantity cannot be negative")
private Integer quantity;
@Email(message = "Invalid email")
private String supplierEmail;
// Constructors, getters, setters
public Product() {}
public Product(String name, Double price, Integer quantity) {
this.name = name;
this.price = price;
this.quantity = quantity;
}
// Getters and Setters
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Double getPrice() {
return price;
}
public void setPrice(Double price) {
this.price = price;
}
public Integer getQuantity() {
return quantity;
}
public void setQuantity(Integer quantity) {
this.quantity = quantity;
}
public String getSupplierEmail() {
return supplierEmail;
}
public void setSupplierEmail(String supplierEmail) {
this.supplierEmail = supplierEmail;
}
}
Usage in Controller with @Valid
package com.example.demo.controller;
import com.example.demo.model.Product;
import com.example.demo.service.ProductService;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/products")
public class ProductController {
private final ProductService productService;
public ProductController(ProductService productService) {
this.productService = productService;
}
// CREATE with validation
@PostMapping
public ResponseEntity<Product> createProduct(@Valid @RequestBody Product product) {
Product created = productService.createProduct(product);
return ResponseEntity.status(HttpStatus.CREATED).body(created);
}
// UPDATE with validation
@PutMapping("/{id}")
public ResponseEntity<Product> updateProduct(@PathVariable Long id,
@Valid @RequestBody Product product) {
Product updated = productService.updateProduct(id, product);
return ResponseEntity.ok(updated);
}
// GET - All
@GetMapping
public ResponseEntity<List<Product>> getAllProducts() {
return ResponseEntity.ok(productService.getAllProducts());
}
// GET - By ID
@GetMapping("/{id}")
public ResponseEntity<Product> getProductById(@PathVariable Long id) {
Product product = productService.getProductById(id);
return ResponseEntity.ok(product);
}
// DELETE
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteProduct(@PathVariable Long id) {
productService.deleteProduct(id);
return ResponseEntity.noContent().build();
}
}
Key Points:
@Validtriggers automatic validation- If validation fails, Spring automatically returns a 400 Bad Request code
Handling Validation Errors
Automatic Response (default)
When validation fails, Spring automatically returns:
{
"timestamp": "2025-11-28T10:15:30.000+00:00",
"status": 400,
"error": "Bad Request",
"message": "Validation failed",
"errors": [
{
"field": "name",
"message": "Name cannot be empty"
},
{
"field": "price",
"message": "Price cannot be negative"
}
]
}
Customization with @ControllerAdvice
package com.example.demo.exception;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import java.util.HashMap;
import java.util.Map;
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidationExceptions(
MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getAllErrors().forEach((error) -> {
String fieldName = ((FieldError) error).getField();
String errorMessage = error.getDefaultMessage();
errors.put(fieldName, errorMessage);
});
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errors);
}
}
Custom Response:
{
"name": "Name cannot be empty",
"price": "Price cannot be negative"
}
Custom Messages
Approach 1: Directly in the Annotation
@NotBlank(message = "Product name is required")
@Size(min = 3, max = 50, message = "Name must be between {min} and {max} characters")
private String name;
Approach 2: Message File (messages.properties)
File src/main/resources/messages.properties:
product.name.notblank=Product name is required
product.name.size=Name must be between {min} and {max} characters
product.price.min=Price cannot be less than {value}
Entity:
@NotBlank(message = "{product.name.notblank}")
@Size(min = 3, max = 50, message = "{product.name.size}")
private String name;
@Min(value = 0, message = "{product.price.min}")
private Double price;
Custom Validation
Create a Custom Annotation
package com.example.demo.validation;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.*;
@Documented
@Constraint(validatedBy = PhoneNumberValidator.class)
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidPhoneNumber {
String message() default "Invalid phone number";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
Create the Validator
package com.example.demo.validation;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
public class PhoneNumberValidator implements ConstraintValidator<ValidPhoneNumber, String> {
@Override
public void initialize(ValidPhoneNumber constraintAnnotation) {
}
@Override
public boolean isValid(String phoneNumber, ConstraintValidatorContext context) {
if (phoneNumber == null) {
return false;
}
// Canadian format: (XXX) XXX-XXXX or XXX-XXX-XXXX
return phoneNumber.matches("\\(?\\d{3}\\)?[- ]?\\d{3}[- ]?\\d{4}");
}
}
Usage
@Entity
public class Customer {
@Id
@GeneratedValue
private Long id;
@ValidPhoneNumber(message = "Phone number must be in Canadian format")
private String phoneNumber;
// Getters, setters
}
Validation Groups
Allows different validation depending on the context (creation vs update).
public interface CreateValidation {}
public interface UpdateValidation {}
@Entity
public class Product {
@Id
@GeneratedValue
@Null(groups = CreateValidation.class, message = "ID must be null on creation")
@NotNull(groups = UpdateValidation.class, message = "ID is required for update")
private Long id;
@NotBlank(groups = {CreateValidation.class, UpdateValidation.class})
private String name;
}
// Controller
@PostMapping
public ResponseEntity<Product> create(@Validated(CreateValidation.class) @RequestBody Product product) {
// ...
}
@PutMapping("/{id}")
public ResponseEntity<Product> update(@PathVariable Long id,
@Validated(UpdateValidation.class) @RequestBody Product product) {
// ...
}
Chapter Summary
Key Points
- Bean Validation (JSR 380) for data validation
- @Valid in the controller to trigger validation
- Custom messages in annotations or properties files
- @ControllerAdvice for global error handling
- Custom validation with custom annotations
Essential Annotations
✅ @NotNull, @NotEmpty, @NotBlank
✅ @Size, @Min, @Max
✅ @Email, @Pattern
✅ @Past, @Future
✅ @Valid