Skip to main content

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​

AnnotationUsageExample
@NotNullCannot be null@NotNull private String name;
@NotEmptyCannot be null or empty@NotEmpty private String email;
@NotBlankCannot be null, empty, or just spaces@NotBlank private String title;
@SizeMin/max size@Size(min=3, max=50) private String name;
@MinMinimum value@Min(0) private int age;
@MaxMaximum value@Max(100) private int grade;
@EmailValid email format@Email private String email;
@PatternRegular expression@Pattern(regexp="[0-9]{10}") private String phone;
@PastDate in the past@Past private LocalDate birthDate;
@FutureDate 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:

  • @Valid triggers 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


Next Chapter: HTTP Error Handling

Back to Module 3