· Jose Antonio López · 4 min lectura
Crear validadores personalizados con Hibernate y Spring Boot
Aprende a implementar validaciones personalizadas en Java con Hibernate Validator, Bean Validation y Spring Boot.

Objetivo
La validación de datos es una parte crítica de cualquier aplicación. Aunque Java Bean Validation (JSR 303/JSR 380) proporciona anotaciones útiles como @NotNull, @Size o @Email, a menudo se necesitan reglas de negocio más específicas.
En este artículo se describe cómo crear un validador personalizado para asegurar que las contraseñas cumplan con criterios de seguridad específicos (longitud mínima, mayúsculas, números y caracteres especiales). Para aprender a gestionar de forma profesional las excepciones lanzadas por estos validadores, se recomienda consultar la guía de manejo global de excepciones.
Prerrequisitos
Para seguir este artículo se requiere tener instalado:
Configuración del Proyecto (build.gradle)
Para este artículo se utiliza Gradle. A continuación se muestran las líneas más relevantes para construir un proyecto con soporte de validación y 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"
}
}Implementación
Directorio y estructura de archivos
Siguiendo una estructura de proyecto limpia, se organizan los validadores y controladores de la siguiente manera:
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. Definición de la Anotación
Primero se crea la interfaz de la anotación. Esta define dónde se puede aplicar y qué clase se encargará de la lógica de validación.
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. Implementación del Validador
La clase SecurePasswordValidator implementa ConstraintValidator, donde reside la lógica para verificar si la contraseña es segura.
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. Uso del Validador en un DTO
Se aplica la nueva anotación @SecurePassword en un DTO (Data Transfer Object) utilizando un record de Java.
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. Uso en el Controlador
Para que la validación se ejecute al recibir una petición, se utiliza la anotación @Valid en el controlador de Spring Boot. Se emplea Lombok con @RequiredArgsConstructor para simplificar la inyección de dependencias.
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);
}
}Tests Unitarios
Es fundamental probar que el validador funcione correctamente para todos los casos:
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));
}
}Referencias
- Hibernate
- Spring Boot