Validations in Spring Boot

Himani Prasad
6 min readAug 23, 2023

--

Photo by mari lezhava on Unsplash

Validation is like a quality check for data. Just like a teacher marks your answers right or wrong, validation checks if the information people give to your program is correct and follows the rules.

Spring Boot provides various mechanisms for validation, including annotations, custom validators, error handling and group validation.

Validation Annotations

In Spring Boot, validation is made easier with annotations that mark fields with specific validation rules. Let’s consider an example of validating a simple registration form for a user:

public class UserRegistrationForm {
@NotBlank(message = "Please provide a username")
private String username;
@Email(message = "Please provide a valid email address")
private String email;
@Size(min = 8, message = "Password must be at least 8 characters long")
private String password;
// Getters and setters
}
  1. @NotNull: Ensures a field is not null.
  2. @NotBlank: Enforces non-nullity and requires at least one non-whitespace character.
  3. @NotEmpty: Guarantees that collections or arrays are not empty.
  4. @Min(value): Checks if a numeric field is greater than or equal to the specified minimum value.
  5. @Max(value): Checks if a numeric field is less than or equal to the specified maximum value.
  6. @Size(min, max): Validates if a string or collection size is within a specific range.
  7. @Pattern(regex): Verifies if a field matches the provided regular expression.
  8. @Email: Ensures a field contains a valid email address format.
  9. @Digits(integer, fraction): Validates that a numeric field has a specified number of integer and fraction digits.
  10. @Past and @Future : Checks that a date or time field is in the past and future respectively.
  11. @AssertTrue and @AssertFalse: Ensures that a boolean field is true. and false respectively.
  12. @CreditCardNumber: Validates that a field contains a valid credit card number.
  13. @Valid: Triggers validation of nested objects or properties.
  14. @Validated: Specifies validation groups to be applied at the class or method level.

13. @Valid

When you apply the @Valid annotation to a method parameter, Spring Boot automatically triggers validation for that parameter before the method is invoked. It is placed before the object to indicate that it should be validated. This means that the incoming data for that parameter will be validated against the specified validation rules.

@RestController
@RequestMapping("/user")
public class ApiController {

@PostMapping("/create")
public ResponseEntity<String> createUser(@RequestBody @Valid User user, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return ResponseEntity.badRequest().body("Validation failed");
}
userService.saveUser(user);
return ResponseEntity.ok("User created successfully");
}
}

If the data fails validation, Spring Boot will automatically generate validation error messages and associate them with the appropriate fields in the input data. These validation errors are typically captured in a BindingResult object, which you can access to analyze and handle validation failures.

14. @Validated

It was introduced to facilitate validation groups and to provide a mechanism to apply validation rules to specific groups of fields within a bean. Unlike the standard @Valid annotation, which validates the entire bean object, @Validated allows you to specify which validation groups to apply during the validation process.

Validation on Nested Properties

When dealing with complex objects that have nested properties requiring validation, you can use the @Valid annotation along with validation annotations to ensure that both the top-level object and its nested properties are properly validated.

public class Order {
@NotNull
private String orderId;
@Valid
private ShippingAddress shippingAddress;
// Other properties, getters, setters...
}
public class ShippingAddress {
@NotNull
private String street;
@NotNull
@Size(min = 2, max = 50)
private String city;
@NotNull
private String zipCode;
}

Controller Integration

Validation often takes place in the controller, where user input is received.

@Controller
public class UserController {
@PostMapping("/register")
public String registerUser(@Valid UserRegistrationForm form, BindingResult result) {
if (result.hasErrors()) {
return "registrationForm"; // Return back to the form with error messages
}
// If no errors, proceed with user registration
// ...
return "redirect:/login";
}
}

Validation in Controller:

@RestController
@RequestMapping("/api/users")
public class UserController {
@PostMapping
public ResponseEntity<String> createUser(@RequestBody @Valid User user, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
// Handle validation errors
return ResponseEntity.badRequest().body("Validation errors found.");
}
// Process user and create a new user
// ...
return ResponseEntity.ok("User created successfully.");
}
}

In this example, the @Valid annotation is used in the createUser method to validate the User object received in the request body. The BindingResult object is used to capture any validation errors.

Global Exception Handling

Validation errors are inevitable. Spring Boot provides a way to handle them globally:

@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidationException(MethodArgumentNotValidException ex) {
// Create a map of field errors
// Return appropriate error response
}
}

The @ControllerAdvice annotation marks a class as a global exception handler and we handle MethodArgumentNotValidException, which is thrown when validation fails.

Custom Validation:

1. Custom Validation Annotation:

A custom validation annotation is created by defining a new annotation. This annotation specifies the validation rules that you want to apply to fields or methods in your classes.

  • @Target: Defines where the annotation can be applied. In the example, it's specified for fields and methods.
  • @Retention: Specifies how long the annotation should be retained. RUNTIME means it will be available at runtime for validation.
  • @Constraint: Specifies the validator class responsible for implementing the validation logic.
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = CustomValidator.class)
public @interface CustomValidation {
String message() default "Invalid value";

Class<?>[] groups() default {};

Class<? extends Payload>[] payload() default {};
}

2. Custom Validator:

The custom validator is a class that implements the ConstraintValidator interface. In the example, the validator class is CustomValidator, and it implements ConstraintValidator<CustomValidation, String>. This means it's responsible for validating a field annotated with @CustomValidation and of type String.

  • initialize(): This method initializes the validator. You can use it to access any annotation attributes if needed.
  • isValid(): This method performs the actual validation logic. You receive the value of the field being validated ( String in this case) and a ConstraintValidatorContext for customizing validation behavior.
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class CustomValidator implements ConstraintValidator<CustomValidation, String> {
@Override
public void initialize(CustomValidation constraintAnnotation) {
}

@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
// Implement your validation logic here
// Return true if validation passes, false otherwise
return value != null && value.startsWith("ABC"); // Example validation condition
}
}

3. Using the Custom Validation Annotation:

You apply your custom validation annotation to fields in your classes. In the example, the Data class has a field called customField that's annotated with @CustomValidation. This annotation triggers the validation logic defined in the associated validator (CustomValidator).

public class Data {
@CustomValidation
private String customField;
}

Validation Groups:

Validation groups allow you to apply specific validation rules to different scenarios. Let’s consider an example where you have a User Registration form with basic and advanced information:

Step 1: Define Validation Groups

Create marker interfaces for different validation groups:

public interface BasicInfo {}
public interface AdvancedInfo {}
public class User {

@NotNull(groups = BasicInfo.class)
@NotNull(groups = AdvancedInfo.class)
private String username;

@NotNull(groups = AdvancedInfo.class)
private String email;
// Other fields, getters, setters
}

Step 2: Apply Validation Groups

Use the @Validated annotation along with the desired validation group in your controller methods:

import org.springframework.validation.annotation.Validated;
@Controller
@Validated
public class UserController {
@PostMapping("/registerBasicInfo")
public String registerBasicInfo(@Validated(BasicInfo.class) @ModelAttribute UserRegistrationForm form, BindingResult result) {
if (result.hasErrors()) {
return "basicInfoForm";
}
// Process basic info registration
return "redirect:/success";
}
@PostMapping("/registerAdvancedInfo")
public String registerAdvancedInfo(@Validated(AdvancedInfo.class) @ModelAttribute UserRegistrationForm form, BindingResult result) {
if (result.hasErrors()) {
return "advancedInfoForm";
}
// Process advanced info registration
return "redirect:/success";
}
}

Validation in the Service Layer

At times, programmatic validation of objects is necessary. Recently, I encountered a task where images were included in the request body. I received the request as a string, which made it impossible to perform validations in the controller directly. To address this, I implemented validation checks in the service layer using a Validator class.

Here's how it can be utilized:

import org.springframework.stereotype.Service;

@Service
@AllArgsContructor
public class UserService {
private final Validator validator;
public void createUser(User user) {
Set<ConstraintViolation<User>> violations = validator.validate(user);
if (!violations.isEmpty()) {
// Handle validation errors
}
}
}

BindingResult

In another task, I needed to store validation failures in a database. While using @Valid allows us to throw exceptions for handling, there are cases where immediate error handling without throwing exceptions is preferred. In such cases, utilizing BindingResult can help capture and store errors directly after the request. Put it right after your Request body.

@RestController
@RequestMapping("/api")
public class ValidationController {

@Autowired
private Validator validator; // Autowire the validator

@PostMapping("/validate")
public ResponseEntity<String> validateData(@RequestBody @Valid Data data, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
for (ObjectError error : bindingResult.getAllErrors()) {
String errorMessage = error.getDefaultMessage();
}
return ResponseEntity.badRequest().body("Validation failed");
}
return ResponseEntity.ok("Data validated and processed successfully");
}
}

Buy me a coffee ☕️: https://www.buymeacoffee.com/himaniprasad

--

--