404 Not Found

404 Not Found


nginx

Data Validation

Data validation is the first line of defense for an API—invalid input should not reach the business logic, and the sooner it is rejected, the better.

1. What You'll Learn


2. A True Story from an API Developer

(1) Pain Point: Dirty Data Being Stored in the Database

Alice discovered some strange data in the OrderFlow database: an order quantity of -5, an email address formatted as "abc," and a product price of 0. Bob reported that a user had submitted a product with negative inventory via the API, causing anomalies in the report statistics. Alice had previously written a lot of if-else validation code in the service layer, which was both verbose and prone to oversights.

(2) The Bean Validation Solution

Declare validation rules using annotations, and Spring automatically triggers validation:

JAVA
public record CreateOrderRequest(
    @NotNull Long productId,
    @Min(1) @Max(100) Integer quantity,
    @Email String customerEmail
) {}

Invalid requests are rejected before they reach the controller.

(3) Revenue

Alice replaced all manual validation code with Bean Validation, reducing the amount of code in the controller by 40% and ensuring that no validation rules are ever overlooked. There is no longer any dirty data in the database.


3. The Bean Validation Annotation System

(1) Verify the Execution Flow

100%
graph TD
    A["Client Request<br/>@RequestBody"] --> B{"@Valid<br/>Triggered?"}
    B -->|Yes| C["Hibernate Validator<br/>Check Constraints"]
    C --> D{"All Valid?"}
    D -->|Yes| E["Controller Method<br/>Executes"]
    D -->|No| F["MethodArgumentNotValidException<br/>400 Bad Request"]
    B -->|No| G["Skip Validation<br/>Potential dirty data"]

(3) Verify the trigger method

Note Purpose Location
@Valid Trigger cascading validation (including nested objects) Method parameters, fields
@Validated Supports group verification Class and method parameters
@Validated(Group.class) Specify Validation Group Method Parameters

(1) ▶ Example: Controller Parameter Validation

JAVA
@RestController
@RequestMapping("/api/v1/orders")
public class OrderController {

    @PostMapping
    public ResponseEntity<Order> createOrder(
            @Valid @RequestBody CreateOrderRequest request) {
        // If validation fails, MethodArgumentNotValidException is thrown
        // before reaching this line
        Order order = orderService.createOrder(request);
        return ResponseEntity.status(HttpStatus.CREATED).body(order);
    }
}

public record CreateOrderRequest(
    @NotNull(message = "Product ID is required")
    Long productId,

    @Min(value = 1, message = "Quantity must be at least 1")
    @Max(value = 100, message = "Quantity cannot exceed 100")
    Integer quantity,

    @Email(message = "Invalid email format")
    String customerEmail
) {}

Output:

TEXT
// Execution Successful

4. Common Constraint Annotations

(1) Quick Reference for Constraint Annotations

Note Applicable Type Description Example
@NotNull All types Cannot be null @NotNull Long id
@NotBlank String Cannot be empty/null/all spaces @NotBlank String name
@NotEmpty String/Collection Cannot be empty/null @NotEmpty List<String> tags
@Size String/Collection Length/Size Range @Size(min=2, max=100)
@Min / @Max Data Type Value Range @Min(0) @Max(99999)
@Positive Numeric type Positive number @Positive BigDecimal price
@Email String Email Format @Email String email
@Pattern String Regular expression match @Pattern(regexp="^[A-Z]")
@Past / @Future Date Type Past/Future @Past LocalDate birthDate

(1) ▶ Example: Product DTO Validation

JAVA
public record CreateProductRequest(
    @NotBlank(message = "Product name is required")
    @Size(min = 2, max = 200, message = "Name must be 2-200 characters")
    String name,

    @NotNull(message = "Price is required")
    @Positive(message = "Price must be positive")
    @DecimalMin(value = "0.01", message = "Price must be at least 0.01")
    BigDecimal price,

    @NotNull(message = "Stock is required")
    @Min(value = 0, message = "Stock cannot be negative")
    Integer stock,

    @Email(message = "Supplier email must be valid")
    String supplierEmail,

    @Pattern(regexp = "^[A-Z]{3}-\\d{4}$", message = "SKU format: XXX-0000")
    String sku
) {}

Output:

TEXT
// Execution Successful
Annotation Comparison null "" " " "abc"
@NotNull
@NotBlank
@NotEmpty
🔥 Common Mistake: When validating the String type, use @NotBlank rather than @NotNull, because an empty string is usually invalid as well.


5. Custom Validators

(1) Implementation Steps

  1. Define Constraint Annotations
  2. Implementation ConstraintValidator<A, T>
  3. Use on DTO fields

(1) ▶ Example: Custom @ValidOrderQuantity validator

JAVA
// Step 1: Define constraint annotation
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = OrderQuantityValidator.class)
public @interface ValidOrderQuantity {
    String message() default "Order quantity exceeds product stock limit";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

// Step 2: Implement ConstraintValidator
public class OrderQuantityValidator
        implements ConstraintValidator<ValidOrderQuantity, Integer> {

    private static final int MAX_QUANTITY_PER_ITEM = 100;

    @Override
    public boolean isValid(Integer quantity, ConstraintValidatorContext context) {
        if (quantity == null) {
            return true; // Let @NotNull handle null check
        }
        return quantity >= 1 && quantity <= MAX_QUANTITY_PER_ITEM;
    }
}

// Step 3: Use in DTO
public record CreateOrderRequest(
    @NotNull Long productId,
    @ValidOrderQuantity Integer quantity
) {}

Output:

TEXT
// Execution Successful

6. Group Validation

(1) Categorizing Validation Rules by Scenario

"Create" and "Update" scenarios typically require different validation rules:

Scenario Product ID Name Price
Create Generated automatically; not required Required Required
Update Required (to indicate who made the change) Optional Optional

(1) ▶ Example: Group Validation

JAVA
// Define group interfaces
public interface Create {}
public interface Update {}

// DTO with group-aware validation
public record ProductRequest(
    @Null(groups = Create.class, message = "ID must be null for creation")
    @NotNull(groups = Update.class, message = "ID is required for update")
    Long id,

    @NotBlank(groups = Create.class, message = "Name is required for creation")
    @Size(min = 2, max = 200)
    String name,

    @NotNull(groups = Create.class, message = "Price is required for creation")
    @Positive BigDecimal price
) {}

Output:

TEXT
// Execution Successful
JAVA
@RestController
@RequestMapping("/api/v1/products")
public class ProductController {

    @PostMapping
    public ResponseEntity<Product> create(
            @Validated(Create.class) @RequestBody ProductRequest request) {
        // Only Create group validations are applied
        // ...
        return ResponseEntity.status(HttpStatus.CREATED).build();
    }

    @PutMapping("/{id}")
    public ResponseEntity<Product> update(
            @PathVariable Long id,
            @Validated(Update.class) @RequestBody ProductRequest request) {
        // Only Update group validations are applied
        // ...
        return ResponseEntity.ok().build();
    }
}

(2) ▶ Example: Nested Validation

JAVA
public record CreateOrderRequest(
    @NotNull Long productId,
    @ValidOrderQuantity Integer quantity,
    @Valid @NotNull ShippingAddress shippingAddress
) {}

public record ShippingAddress(
    @NotBlank String street,
    @NotBlank String city,
    @NotBlank String zipCode,
    @Pattern(regexp = "^[A-Z]{2}$") String country
) {}

Output:

TEXT
// Execution Successful
📌 Key Point: Nested objects must include @Valid; otherwise, nested field validation will not take effect. @Validated does not support nested cascading validation.


7. Validating the Format of Error Responses

(1) Standardize the error response format

JAVA
@RestControllerAdvice
public class ValidationExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Map<String, Object>> handleValidation(
            MethodArgumentNotValidException ex) {
        Map<String, Object> body = new LinkedHashMap<>();
        body.put("timestamp", Instant.now());
        body.put("status", HttpStatus.BAD_REQUEST.value());

        List<Map<String, String>> errors = ex.getBindingResult()
            .getFieldErrors().stream()
            .map(fe -> Map.of(
                "field", fe.getField(),
                "message", fe.getDefaultMessage() != null ? fe.getDefaultMessage() : "",
                "rejectedValue", fe.getRejectedValue() != null ? fe.getRejectedValue().toString() : "null"
            ))
            .toList();
        body.put("errors", errors);
        return ResponseEntity.badRequest().body(body);
    }
}
💻 Output:

JSON
{
  "timestamp": "2024-01-15T10:00:00Z",
  "status": 400,
  "errors": [
    {"field": "quantity", "message": "Quantity must be at least 1", "rejectedValue": "0"},
    {"field": "customerEmail", "message": "Invalid email format", "rejectedValue": "abc"}
  ]
}

8. Comprehensive Example: The Complete OrderFlow Verification System

JAVA
// Validation groups
package com.orderflow.validation;
public interface Create {}
public interface Update {}

// Custom validator: @ValidShippingAddress
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = ShippingAddressValidator.class)
public @interface ValidShippingAddress {
    String message() default "Invalid shipping address";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

public class ShippingAddressValidator
        implements ConstraintValidator<ValidShippingAddress, String> {
    @Override
    public boolean isValid(String address, ConstraintValidatorContext ctx) {
        if (address == null || address.isBlank()) return false;
        return address.length() >= 10 && address.length() <= 500;
    }
}

// DTOs
public record CreateOrderRequest(
    @NotNull(groups = Create.class) Long productId,
    @Min(value = 1, message = "Quantity must be at least 1")
    @Max(value = 100, message = "Quantity cannot exceed 100")
    Integer quantity,
    @Email String customerEmail,
    @ValidShippingAddress String shippingAddress
) {}

public record CreateProductRequest(
    @Null(groups = Create.class) @NotNull(groups = Update.class) Long id,
    @NotBlank(groups = Create.class) @Size(min = 2, max = 200) String name,
    @NotNull(groups = Create.class) @Positive BigDecimal price,
    @Min(0) Integer stock,
    @Pattern(regexp = "^[A-Z]{3}-\\d{4}$", message = "SKU: XXX-0000") String sku
) {}

// Controller
@RestController
@RequestMapping("/api/v1/orders")
public class OrderController {
    @PostMapping
    public ResponseEntity<Void> create(
            @Validated(Create.class) @RequestBody CreateOrderRequest req) {
        // Validation passed, proceed to business logic
        return ResponseEntity.status(HttpStatus.CREATED).build();
    }
}

❓ FAQ

Q What is the difference between @Valid and @Validated?
A @Valid is a JSR-380 standard annotation that supports nested and cascading validation. @Validated is a Spring extension annotation that supports grouped validation. Use @Validated for grouping and @Valid for nesting; the two can be used in combination.
Q What is returned when validation fails?
A By default, Spring Boot returns a 400 Bad Request status code along with a JSON error message. You can customize the format using @RestControllerAdvice; this lesson provides a standardized formatting solution.
Q How do I choose between @NotBlank, @NotEmpty, and @NotNull?
A Use @NotBlank for Strings (does not allow null, empty strings, or strings consisting solely of spaces); use @NotEmpty for Collections (does not allow null or empty collections); and use @NotNull for all other types.
Q Can group-based authentication and default authentication be enabled at the same time?
A By default, the "Default" group is disabled once a specific group is specified. If you want both to be enabled, have the custom group inherit from "Default": public interface Create extends Default {}.
Q Can Spring Beans be injected into custom validators?
A Yes. ConstraintValidators are managed by the Spring container, so you can use @Autowired to inject Beans in the isValid method (e.g., to query the database for uniqueness validation).
Q How do I internationalize error messages?
A Define message keys in resources/ValidationMessages.properties, such as order.quantity.invalid=Order quantity must be between {min} and {max}; multilingual files are supported.

📖 Summary


📝 Exercises

  1. Basic Exercise (Difficulty: ⭐): Add Bean Validation annotations to OrderFlow's CreateOrderRequest and CreateProductRequest to validate invalid input and return a 400 error.

  2. Advanced Exercise (Difficulty ⭐⭐): Implement grouped validation—for Create operations, name and price are required; for Update operations, id is required and name and price are optional. Implement a custom @ValidShippingAddress validator.

  3. Challenge (Difficulty: ⭐⭐⭐): Create a @UniqueProductSku validator that injects the ProductRepository to check whether an SKU already exists, implementing database-level uniqueness validation. Consider the boundary between the responsibilities of the validator and the business layer.

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏