· Jose Antonio López · 4 min read
How to Create Custom Validators with Hibernate and Spring Boot
Learn how to implement custom validations in Java with Hibernate Validator, Bean Validation, and Spring Boot.

Objective
Data validation is a critical part of any application. Although Java Bean Validation (JSR 303/JSR 380) provides useful annotations like @NotNull, @Size, or @Email, more specific business rules are often required.
This article describes how to create a custom validator to ensure that passwords meet specific security criteria (minimum length, uppercase letters, numbers, and special characters). To learn how to professionally handle exceptions thrown by these validators, it is recommended to consult the global exception handling guide.
Prerequisites
To follow this article, the following must be installed:
Project Configuration (build.gradle)
Gradle is used for this article. Below are the most relevant lines to build a project with validation support and Hibernate Validator:
plugins {
id 'org.springframework.boot' version '3.5.1'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java'
}
group = 'com.safeboxes'
version = '1.0.0'
sourceCompatibility = '21'
repositories {
mavenCentral()
}
dependencies {
// Spring Boot
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
// Testing
testImplementation 'org.springframework.boot:spring-boot-starter-test'
// Third-party libraries
implementation 'org.hibernate.validator:hibernate-validator:9.1.0.Final'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
}
tasks.named('test') {
useJUnitPlatform()
}
test {
testLogging {
events "PASSED", "SKIPPED", "FAILED"
}
}Implementation
Directory and File Structure
Following a clean project structure, validators and controllers are organized as follows:
src
├── main
│ └── java
│ └── com/safeboxes
│ ├── config
│ │ └── ApiConfig.java
│ ├── domain
│ │ ├── exceptions
│ │ │ ├── SafeBoxException.java
│ │ │ └── enums
│ │ │ └── SafeBoxErrorMessage.java
│ │ ├── services
│ │ │ └── SafeBoxService.java
│ │ └── validators
│ │ ├── SecurePassword.java
│ │ └── SecurePasswordValidator.java
│ ├── dtos
│ │ └── safes
│ │ └── CreateSafeBoxDTO.java
│ └── infrastructure
│ └── controllers
│ └── SafeBoxController.java
└── test
└── java
└── com/safeboxes
└── domain/validators
└── SecurePasswordValidatorTest.java1. Annotation Definition
First, the annotation interface is created. This defines where it can be applied and which class will handle the validation logic.
package com.safeboxes.domain.validators;
import jakarta.validation.Constraint;
import jakarta.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, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = {SecurePasswordValidator.class})
public @interface SecurePassword {
String message() default "Invalid Password";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}2. Validator Implementation
The SecurePasswordValidator class implements ConstraintValidator, where the logic to verify if the password is secure resides.
package com.safeboxes.domain.validators;
import com.safeboxes.domain.exceptions.SafeBoxException;
import com.safeboxes.domain.exceptions.enums.SafeBoxErrorMessage;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import java.util.regex.Pattern;
public class SecurePasswordValidator implements ConstraintValidator<SecurePassword, String> {
@Override
public boolean isValid(String password, ConstraintValidatorContext context) {
if (!isSecurePassword(password)) {
SafeBoxErrorMessage error = SafeBoxErrorMessage.INVALID_PASSWORD_FORMAT;
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate(SafeBoxErrorMessage.getMessage(error));
throw new SafeBoxException(error);
}
return true;
}
private boolean isSecurePassword(String password) {
final int MINIMUM_PASSWORD_LENGTH = 12;
if (password == null || password.length() < MINIMUM_PASSWORD_LENGTH) {
return false;
}
final String UPPER_CASE_REG_EX = ".*[A-Z].*";
final String LOWER_CASE_REG_EX = ".*[a-z].*";
final String DIGIT_REG_EX = ".*\\d.*";
final String SPECIAL_CHAR_REG_EX = ".*[! @#$%^&*()].*";
return Pattern.matches(UPPER_CASE_REG_EX, password) &&
Pattern.matches(LOWER_CASE_REG_EX, password) &&
Pattern.matches(DIGIT_REG_EX, password) &&
Pattern.matches(SPECIAL_CHAR_REG_EX, password);
}
}3. Using the Validator in a DTO
The new @SecurePassword annotation is applied in a DTO (Data Transfer Object) using a Java record.
package com.safeboxes.dtos.safes;
import com.safeboxes.domain.validators.SecurePassword;
import jakarta.validation.constraints.NotNull;
public record CreateSafeBoxDTO(
@NotNull String name,
@NotNull @SecurePassword String password
) {
}4. Using in the Controller
To execute the validation when receiving a request, the @Valid annotation is used in the Spring Boot controller. Lombok with @RequiredArgsConstructor is employed to simplify dependency injection.
package com.safeboxes.infrastructure.controllers;
import com.safeboxes.config.ApiConfig;
import com.safeboxes.domain.services.SafeBoxService;
import com.safeboxes.dtos.safes.CreateSafeBoxDTO;
import com.safeboxes.dtos.safes.SafeBoxDTO;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import lombok.RequiredArgsConstructor;
import jakarta.validation.Valid;
@RestController
@RequestMapping(value = ApiConfig.BASE_PATH + "/safeboxes")
@RequiredArgsConstructor
public class SafeBoxController {
private final SafeBoxService safeBoxService;
@PostMapping
public SafeBoxDTO createSafeBox(@Valid @RequestBody CreateSafeBoxDTO request) {
return safeBoxService.createSafeBox(request);
}
}Unit Tests
It is essential to test that the validator works correctly for all cases:
package com.safeboxes.domain.validators;
import com.safeboxes.domain.exceptions.SafeBoxException;
import com.safeboxes.domain.exceptions.enums.SafeBoxErrorMessage;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.NullSource;
import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.Answers;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import jakarta.validation.ConstraintValidatorContext;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
@ExtendWith(MockitoExtension.class)
class SecurePasswordValidatorTest {
@InjectMocks
private SecurePasswordValidator passwordValidator;
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
private ConstraintValidatorContext context;
@ParameterizedTest
@ValueSource(strings = {
"short", // Too short
"123456789012", // No letters
"abcdefghijkl", // No upper case
"ABCDEFGHIJKL", // No lower case
"Abcdefghijkl", // No digits
"Abcdefghijk1", // No special char
"Abcdefghijk!", // No digit
" ", // Spaces
})
@NullSource
@DisplayName("Invalid passwords should throw SafeBoxException")
void invalid_passwords_should_throw_exception(String password) {
SafeBoxException exception = assertThrows(SafeBoxException.class, () -> {
passwordValidator.isValid(password, context);
});
assertEquals(SafeBoxErrorMessage.INVALID_PASSWORD_FORMAT, exception.getErrorMessage());
}
@ParameterizedTest
@ValueSource(strings = {
"ValidPass123!",
"Secure#2026Password",
"A1b2C3d4E5f!",
"PasswordWithSpace 1!"
})
@DisplayName("Valid passwords should return true")
void valid_passwords_should_return_true(String password) {
assertTrue(passwordValidator.isValid(password, context));
}
@Test
@DisplayName("should return true for a secure password")
void secure_password_ok() {
final String testPassword = "ValidPassword123!";
assertTrue(passwordValidator.isValid(testPassword, context));
}
}References
- Hibernate
- Spring Boot