🌱 Spring Boot Q29 / 69

How do you handle validation in Spring Boot REST APIs?

AI-Powered Answer ✓ Answered

Validation is a critical aspect of building robust REST APIs, ensuring data integrity and user experience. Spring Boot seamlessly integrates with the Jakarta Bean Validation API (JSR 303/380), providing a powerful and flexible framework for validating incoming request data.

Required Dependency

To enable validation features in your Spring Boot application, you need to include the spring-boot-starter-validation dependency. This starter automatically brings in Hibernate Validator, which is the reference implementation of the Bean Validation API.

xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

Annotation-based Validation

The most common approach to validation is by using predefined annotations on your Data Transfer Objects (DTOs) or domain models. These annotations specify various constraints on fields, such as nullability, size, format, and range. To trigger validation for a request body, you apply @Valid or @Validated to the DTO parameter in your controller method.

  • @NotNull, @NotEmpty, @NotBlank: For checking non-null, non-empty, and non-blank string values respectively.
  • @Size: For specifying the min and max length of a string or collection.
  • @Min, @Max: For numerical range constraints.
  • @Email: For validating email format.
  • @Pattern: For validating against a regular expression.
  • @Future, @Past: For date validation.
java
import jakarta.validation.constraints.*;
import java.math.BigDecimal;

public class ProductRequest {
    @NotBlank(message = "Product name is required")
    @Size(min = 2, max = 100, message = "Product name must be between 2 and 100 characters")
    private String name;

    @NotNull(message = "Price is required")
    @DecimalMin(value = "0.01", message = "Price must be positive")
    private BigDecimal price;

    @Email(message = "Invalid email format")
    @Size(max = 255, message = "Email too long")
    private String contactEmail;

    // Getters and Setters
}

In your Spring Boot REST controller, you then apply @Valid to the request body parameter to enable validation.

java
import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/products")
public class ProductController {
    @PostMapping
    public ResponseEntity<String> createProduct(@Valid @RequestBody ProductRequest productRequest) {
        // If validation passes, productRequest is valid
        // Process the valid productRequest
        return ResponseEntity.ok("Product created successfully!");
    }
}

Custom Validation

For complex business rules that cannot be expressed with standard annotations, you can create custom validation annotations. This involves two main components:

  • A custom annotation definition (e.g., @ValidProductId) annotated with @Constraint which links to the validator class.
  • A ConstraintValidator implementation (e.g., ProductIdValidator) that contains the actual validation logic.

Error Handling for Validation Failures

When validation fails, Spring Boot throws a MethodArgumentNotValidException. To provide a user-friendly and consistent error response, it's best practice to implement a global exception handler using @ControllerAdvice and @ExceptionHandler.

java
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 org.springframework.web.bind.annotation.ResponseStatus;

import java.util.HashMap;
import java.util.Map;

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    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 new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST);
    }
}

Validation Groups

Validation groups allow you to apply different sets of validation rules based on the context, such as creating a resource versus updating it. You define marker interfaces for each group and then specify these groups in your validation annotations. When invoking validation, you pass the desired group to the @Validated annotation on your controller method.

  • Define marker interfaces: public interface OnCreate {}, public interface OnUpdate {}.
  • Apply groups to annotations: @NotBlank(groups = OnCreate.class).
  • Trigger validation with groups: @Validated(OnCreate.class) @RequestBody ProductRequest productRequest.

By leveraging these features, Spring Boot enables you to build REST APIs with robust and maintainable data validation, significantly improving the quality and reliability of your applications.