Agrupar Restricciones de Validación en Jakarta Bean Validation

¿Cómo agrupar restricciones de validación en Jakarta Bean Validation en JAVA?

1. Introducción

En nuestro tutorial sobre Fundamentos de Java Bean Validation, vimos el uso de diversas restricciones integradas de jakarta.validation. En este tutorial, exploraremos cómo agrupar las restricciones de jakarta.validation. Agrupar restricciones no solo ayuda a organizar nuestro código, sino que también permite validar diferentes conjuntos de datos de forma más eficiente, lo que es especialmente útil en aplicaciones complejas.

2. Caso de Uso

Existen muchos escenarios en los que necesitamos aplicar restricciones a un conjunto determinado de campos de un bean y luego, más tarde, aplicar restricciones a otro conjunto de campos del mismo bean. Por ejemplo, imaginemos que tenemos un formulario de registro en dos pasos. En el primer paso, pedimos al usuario que proporcione información básica como su nombre, apellido, dirección de correo electrónico y número de teléfono. Cuando el usuario envía estos datos, queremos validar únicamente esta información.

En el siguiente paso, solicitamos al usuario que proporcione otra información, como su dirección, y queremos validar también esta información. Cabe señalar que el captcha está presente en ambos pasos, lo que complica la validación.

3. Agrupando Restricciones de Validación

Todas las restricciones de jakarta.validation tienen un atributo llamado groups. Cuando agregamos una restricción a un elemento, podemos declarar el nombre del grupo al que pertenece la restricción. Esto se realiza especificando el nombre de la clase de la interfaz del grupo en el atributo groups de la restricción.

La mejor forma de entender esto es poniendo manos a la obra. Veamos cómo combinar las restricciones de jakarta en grupos.

3.1. Declarando Grupos de Restricción

El primer paso es crear algunas interfaces. Estas interfaces serán los nombres de los grupos de restricción. En nuestro caso de uso, estamos dividiendo las restricciones de validación en dos grupos.

Veamos el primer grupo de restricciones, BasicInfo:

public interface BasicInfo {
}

El siguiente grupo de restricciones es AdvanceInfo:

public interface AdvanceInfo {
}

3.2. Usando Grupos de Restricción

Ahora que hemos declarado nuestros grupos de restricciones, es momento de usarlos en nuestra clase bean “RegistrationForm”:

public class RegistrationForm {
    @NotBlank(groups = BasicInfo.class)
    private String firstName;
    
    @NotBlank(groups = BasicInfo.class)
    private String lastName;
    
    @Email(groups = BasicInfo.class)
    private String email;
    
    @NotBlank(groups = BasicInfo.class)
    private String phone;

    @NotBlank(groups = {BasicInfo.class, AdvanceInfo.class})
    private String captcha;

    @NotBlank(groups = AdvanceInfo.class)
    private String street;
    
    @NotBlank(groups = AdvanceInfo.class)
    private String houseNumber;
    
    @NotBlank(groups = AdvanceInfo.class)
    private String zipCode;
    
    @NotBlank(groups = AdvanceInfo.class)
    private String city;
    
    @NotBlank(groups = AdvanceInfo.class)
    private String country;
}

Con el atributo groups, hemos dividido los campos de nuestro bean en dos grupos según nuestro caso de uso. Por defecto, todas las restricciones se incluyen en el grupo de restricciones Default.

3.3. Probando Restricciones de un Grupo

Ahora que hemos declarado y utilizado los grupos de restricciones en nuestra clase bean, es momento de ver a estos grupos de restricciones en acción.

Primero, observaremos qué sucede cuando la información básica no está completa, utilizando nuestro grupo de restricciones BasicInfo para la validación. Deberíamos recibir una violación de restricciones para cualquier campo dejado en blanco donde se utilizó BasicInfo.class en el atributo groups de la restricción @NotBlank:

public class RegistrationFormUnitTest {
    private static Validator validator;

    @BeforeClass
    public static void setupValidatorInstance() {
        validator = Validation.buildDefaultValidatorFactory().getValidator();
    }

    @Test
    public void whenBasicInfoIsNotComplete_thenShouldGiveConstraintViolationsOnlyForBasicInfo() {
        RegistrationForm form = buildRegistrationFormWithBasicInfo();
        form.setFirstName("");

        Set> violations = validator.validate(form, BasicInfo.class);

        assertThat(violations.size()).isEqualTo(1);
        violations.forEach(action -> {
            assertThat(action.getMessage()).isEqualTo("must not be blank");
            assertThat(action.getPropertyPath().toString()).isEqualTo("firstName");
        });
    }

    private RegistrationForm buildRegistrationFormWithBasicInfo() {
        RegistrationForm form = new RegistrationForm();
        form.setFirstName("devender");
        form.setLastName("kumar");
        form.setEmail("devender@example.com");
        form.setPhone("12345");
        form.setCaptcha("Y2HAhU5T");
        return form;
    }

    // ... pruebas adicionales
}

En el siguiente escenario, verificaremos qué sucede cuando la información avanzada está incompleta, utilizando nuestro grupo de restricciones AdvanceInfo para la validación:

@Test
public void whenAdvanceInfoIsNotComplete_thenShouldGiveConstraintViolationsOnlyForAdvanceInfo() {
    RegistrationForm form = buildRegistrationFormWithAdvanceInfo();
    form.setZipCode("");

    Set> violations = validator.validate(form, AdvanceInfo.class);

    assertThat(violations.size()).isEqualTo(1);
    violations.forEach(action -> {
        assertThat(action.getMessage()).isEqualTo("must not be blank");
        assertThat(action.getPropertyPath().toString()).isEqualTo("zipCode");
    });
}

private RegistrationForm buildRegistrationFormWithAdvanceInfo() {
    RegistrationForm form = new RegistrationForm();
    return populateAdvanceInfo(form);
}

private RegistrationForm populateAdvanceInfo(RegistrationForm form) {
    form.setCity("Berlin");
    form.setCountry("DE");
    form.setStreet("alexa str.");
    form.setZipCode("19923");
    form.setHouseNumber("2a");
    form.setCaptcha("Y2HAhU5T");
    return form;
}

3.4. Probando Restricciones con Múltiples Grupos

Podemos especificar múltiples grupos para una restricción. En nuestro caso de uso, utilizamos captcha en ambos, información básica y avanzada. Primero, probaremos el captcha con BasicInfo:

@Test
public void whenCaptchaIsBlank_thenShouldGiveConstraintViolationsForBasicInfo() {
    RegistrationForm form = buildRegistrationFormWithBasicInfo();
    form.setCaptcha("");

    Set> violations = validator.validate(form, BasicInfo.class);

    assertThat(violations.size()).isEqualTo(1);
    violations.forEach(action -> {
        assertThat(action.getMessage()).isEqualTo("must not be blank");
        assertThat(action.getPropertyPath().toString()).isEqualTo("captcha");
    });
}

Ahora probemos el captcha con AdvanceInfo:

@Test
public void whenCaptchaIsBlank_thenShouldGiveConstraintViolationsForAdvanceInfo() {
    RegistrationForm form = buildRegistrationFormWithAdvanceInfo();
    form.setCaptcha("");

    Set> violations = validator.validate(form, AdvanceInfo.class);

    assertThat(violations.size()).isEqualTo(1);
    violations.forEach(action -> {
        assertThat(action.getMessage()).isEqualTo("must not be blank");
        assertThat(action.getPropertyPath().toString()).isEqualTo("captcha");
    });
}

4. Especificar el Orden de Validación de Grupos de Restricción con GroupSequence

Por defecto, los grupos de restricciones no se evalúan en un orden particular. Pero podemos tener casos de uso donde algunos grupos deben validarse antes que otros. Para lograr esto, podemos especificar el orden de validación del grupo utilizando GroupSequence.

Hay dos formas de usar la anotación GroupSequence:

  • En la entidad que se está validando.
  • En una interfaz.

4.1. Usando GroupSequence en la Entidad que se Está Validando

Esta es una manera simple de ordenar las restricciones. Vamos a anotar la entidad con GroupSequence y especificar el orden de las restricciones:

@GroupSequence({BasicInfo.class, AdvanceInfo.class})
public class RegistrationForm {
    @NotBlank(groups = BasicInfo.class)
    private String firstName;

    @NotBlank(groups = AdvanceInfo.class)
    private String street;
}

4.2. Usando GroupSequence en una Interfaz

También podemos especificar el orden de validación de restricciones usando una interfaz. La ventaja de este enfoque es que la misma secuencia se puede usar para otras entidades. Veamos cómo podemos usar GroupSequence con las interfaces que definimos anteriormente:

@GroupSequence({BasicInfo.class, AdvanceInfo.class})
public interface CompleteInfo {
}

4.3. Probando GroupSequence

Ahora probemos GroupSequence. Primero, verificaremos que si BasicInfo está incompleto, entonces la restricción del grupo AdvanceInfo no se evaluará:

@Test
public void whenBasicInfoIsNotComplete_thenShouldGiveConstraintViolationsForBasicInfoOnly() {
    RegistrationForm form = buildRegistrationFormWithBasicInfo();
    form.setFirstName("");

    Set> violations = validator.validate(form, CompleteInfo.class);

    assertThat(violations.size()).isEqualTo(1);
    violations.forEach(action -> {
        assertThat(action.getMessage()).isEqualTo("must not be blank");
        assertThat(action.getPropertyPath().toString()).isEqualTo("firstName");
    });
}

Luego, probamos que cuando BasicInfo esté completo, la restricción AdvanceInfo se evalúe:

@Test
public void whenBasicAndAdvanceInfoIsComplete_thenShouldNotGiveConstraintViolationsWithCompleteInfoValidationGroup() {
    RegistrationForm form = buildRegistrationFormWithBasicAndAdvanceInfo();

    Set> violations = validator.validate(form, CompleteInfo.class);

    assertThat(violations.size()).isEqualTo(0);
}

5. Conclusión

En este tutorial rápido, vimos cómo agrupar jakarta.validation restricciones. Agrupar las restricciones nos permite validar diferentes elementos de forma independiente y mejorar la claridad de nuestro código.

Consejos Prácticos

  • Organización: Mantén tus grupos de validación bien organizados y documentados para facilitar la comprensión a otros desarrolladores.
  • Pruebas: Asegúrate de escribir pruebas exhaustivas para cada grupo de restricciones. Esto garantiza que tu validación funcione según lo esperado en diferentes escenarios.
  • Reutilización: Considera reutilizar grupos de validación cuando construyas formularios similares para optimizar el tiempo de desarrollo.

Al aplicar estas prácticas, puedes aprovechar al máximo el sistema de validación de jakarta.validation en Java, haciendo que tus aplicaciones sean más robustas y fáciles de mantener.