Enterprise Java

Developing Stateful Custom Bean Validation Using Spring Boot

Bean Validation is a core feature of Spring Boot used to enforce rules on Java objects before they are processed or persisted. While basic validations like @NotNull or @Email cover most cases, real-world applications often need custom and stateful validations — where validation logic depends on multiple fields, external configurations, or even runtime data. Let us delve into understanding spring custom stateful bean validation and how it can enhance data integrity in your applications.

1. What is Bean Validation in Spring Boot?

Bean Validation is a specification defined under JSR 380 (Jakarta Bean Validation 2.0). It provides a unified way to apply validation rules directly to the fields of Java objects using annotations. Spring Boot integrates seamlessly with this validation framework via the spring-boot-starter-validation dependency, which internally uses Hibernate Validator as the default implementation.

With Bean Validation, you can enforce constraints on your domain models, DTOs, and request payloads without writing explicit validation logic in controllers. When a request is made, Spring automatically invokes these constraints before method execution, ensuring that only valid data reaches your business layer.

1.1 Common Validation Annotations

Here are some frequently used built-in validation annotations provided by the Jakarta Bean Validation API:

  • @NotNull – Ensures a field is not null. Useful for mandatory fields.
  • @Size(min, max) – Validates that a string, array, or collection has a length or size within the specified bounds.
  • @Email – Checks whether a string is in a valid email format.
  • @Pattern(regexp) – Validates that a string matches a specified regular expression.
  • @Min and @Max – Validate numeric ranges for integer or decimal fields.
  • @Past and @Future – Ensure date or time values are in the past or future respectively.

2. Code Example

Below is an example demonstrating how to build custom validations in Spring Boot. The example covers all three types of validations — single-field, multi-field (cross-field), and stateful validations using Spring properties. Each section introduces the concept followed by the respective code snippet. This complete example can be directly used as a working Spring Boot project.

2.1 Add Required Dependencies

To enable validation, include the spring-boot-starter-validation dependency along with spring-boot-starter-web in your pom.xml. The validation starter provides the Jakarta Bean Validation API used by Spring Boot.

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

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

2.2 Define the Main Spring Boot Application Class

The main application class bootstraps the Spring Boot application. Annotating it with @SpringBootApplication enables component scanning and auto-configuration.

package com.example.validationdemo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class ValidationDemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(ValidationDemoApplication.class, args);
    }
}

2.3 Create a Request DTO with Annotations

The DTO class represents the incoming request payload. It uses custom and standard validation annotations on its fields. Here, @ValidUsername ensures that the username starts with a prefix, @ValidPassword enforces password rules from configuration, and @ValidDateRange ensures that startDate is before endDate.

package com.example.validationdemo.dto;

import com.example.validationdemo.validation.*;
import jakarta.validation.constraints.NotNull;
import java.time.LocalDate;

@ValidDateRange
public class UserRequest {

    @NotNull
    @ValidUsername(prefix = "emp_")
    private String username;

    @ValidPassword
    private String password;

    private LocalDate startDate;
    private LocalDate endDate;

    // Getters and Setters
    public String getUsername() { return username; }
    public void setUsername(String username) { this.username = username; }

    public String getPassword() { return password; }
    public void setPassword(String password) { this.password = password; }

    public LocalDate getStartDate() { return startDate; }
    public void setStartDate(LocalDate startDate) { this.startDate = startDate; }

    public LocalDate getEndDate() { return endDate; }
    public void setEndDate(LocalDate endDate) { this.endDate = endDate; }
}

2.4 Build a REST Controller for Validation

The controller defines a REST endpoint to handle POST requests. The @Valid annotation ensures the request body is validated before the method executes. If validation fails, Spring automatically returns a 400 Bad Request response with validation messages.

package com.example.validationdemo.controller;

import com.example.validationdemo.dto.UserRequest;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/users")
public class UserController {

    @PostMapping
    public String createUser(@Valid @RequestBody UserRequest request) {
        return "User Created: " + request.getUsername();
    }
}

2.5 Define a Custom Annotation for Single-Field Validation

This custom annotation @ValidUsername checks whether a field value starts with a given prefix. It uses the Constraint annotation to link to its validator class.

// ValidUsername.java
package com.example.validationdemo.validation;

import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.*;

@Documented
@Constraint(validatedBy = UsernameValidator.class)
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidUsername {
    String message() default "Username must start with prefix";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
    String prefix() default "user_";
}

2.5.1 Implement the Single-Field Validator Logic

The UsernameValidator class implements ConstraintValidator and contains logic to validate whether the provided value starts with the defined prefix. The prefix is read from the annotation.

// UsernameValidator.java
package com.example.validationdemo.validation;

import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;

public class UsernameValidator implements ConstraintValidator<ValidUsername, String> {

    private String prefix;

    @Override
    public void initialize(ValidUsername constraintAnnotation) {
        this.prefix = constraintAnnotation.prefix();
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (value == null) return false;
        return value.startsWith(prefix);
    }
}

2.6 Define a Custom Annotation for Multi-Field Validation

The @ValidDateRange annotation is applied at the class level. It ensures that one field depends on another—in this case, verifying that startDate occurs before endDate.

// ValidDateRange.java
package com.example.validationdemo.validation;

import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.*;

@Documented
@Constraint(validatedBy = DateRangeValidator.class)
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidDateRange {
    String message() default "Start date must be before end date";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

2.6.1 Implement the Multi-Field Validator Logic

The DateRangeValidator implements validation logic that checks whether the startDate precedes the endDate. If either date is null, the check is skipped to allow optional fields.

// DateRangeValidator.java
package com.example.validationdemo.validation;

import com.example.validationdemo.dto.UserRequest;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;

public class DateRangeValidator implements ConstraintValidator<ValidDateRange, UserRequest> {

    @Override
    public boolean isValid(UserRequest request, ConstraintValidatorContext context) {
        if (request.getStartDate() == null || request.getEndDate() == null)
            return true;
        return request.getStartDate().isBefore(request.getEndDate());
    }
}

2.7 Create a Custom Password Validation Using Application Properties

The @ValidPassword annotation defines a field-level constraint. The actual validation rule will rely on a value from application.properties, making it stateful and configurable without code changes.

// ValidPassword.java
package com.example.validationdemo.validation;

import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.*;

@Documented
@Constraint(validatedBy = PasswordPolicyValidator.class)
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidPassword {
    String message() default "Password must meet policy requirements";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

2.7.1 Implement the Stateful Password Validator

The PasswordPolicyValidator uses Spring’s @Value annotation to inject a configuration property for minimum password length. This allows runtime flexibility and application-level policy enforcement.

// PasswordPolicyValidator.java
package com.example.validationdemo.validation;

import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class PasswordPolicyValidator implements ConstraintValidator<ValidPassword, String> {

    @Value("${validation.password.min-length:8}")
    private int minLength;

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (value == null) return false;
        return value.length() >= minLength;
    }
}

2.8 Add Configuration in application.properties

The configuration defines a property to control the minimum password length used by ValidPassword validation. This allows administrators to adjust password policies dynamically.

server.port=8080
validation.password.min-length=6

2.9 Running the Application and Viewing Output

Once the Spring Boot application is up and running on port 8080, you can test your validation logic using curl commands from your terminal. Below are two examples showing how the validation behaves for both valid and invalid inputs.

2.9.1 Example: Valid Request

This example sends a valid JSON payload where all validation rules pass. The username starts with emp_, the password meets the minimum length defined in application.properties, and the start date precedes the end date. The API successfully creates the user and returns a 200 OK response.

curl -X POST http://localhost:8080/api/users \
-H "Content-Type: application/json" \
-d '{
  "username": "emp_john",
  "password": "mypassword",
  "startDate": "2025-01-01",
  "endDate": "2025-01-10"
}'

Response: 200 OK
"User Created: emp_john"

This example shows a successful API request where all validation rules pass. The username starts with the required prefix (emp_), the password meets the minimum length defined in the application properties, and the start date is before the end date. When you send this request, the Spring Boot controller processes it successfully and returns a 200 OK response along with the confirmation message.

2.9.2 Example: Invalid Request and Error Response

This example demonstrates a request that violates multiple validation rules. The username does not begin with the required prefix, the password is shorter than the configured minimum length, and the start date occurs after the end date. Spring Boot automatically responds with a 400 Bad Request and includes detailed validation error messages.

curl -X POST http://localhost:8080/api/users \
-H "Content-Type: application/json" \
-d '{
  "username": "john",
  "password": "123",
  "startDate": "2025-01-10",
  "endDate": "2025-01-01"
}'

Response: 400 Bad Request
{
  "errors": [
    "Username must start with prefix",
    "Password must meet policy requirements",
    "Start date must be before end date"
  ]
}

3. Conclusion

Spring Boot’s Bean Validation framework provides an elegant, extensible way to enforce business rules. By creating custom annotations and validators, you can handle complex scenarios including multi-field checks and validations that depend on external state (such as configuration properties).

Yatin Batra

An experience full-stack engineer well versed with Core Java, Spring/Springboot, MVC, Security, AOP, Frontend (Angular & React), and cloud technologies (such as AWS, GCP, Jenkins, Docker, K8).
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Back to top button