Validación de Objetos de Dominio en Spring Boot

Validación de Objetos de Dominio en Spring Boot

Cuando se trata de validar la entrada del usuario, Spring Boot ofrece un soporte robusto para esta tarea común pero crítica desde el primer momento. Aunque Spring Boot admite la integración sin problemas con validadores personalizados, el estándar de facto para realizar la validación es Hibernate Validator, la implementación de referencia del marco de Validación de Beans.

En este tutorial, veremos cómo validar objetos de dominio en Spring Boot mediante la construcción de un controlador REST básico. A lo largo de este artículo, cubriremos la configuración de las dependencias de Maven, la creación de una clase de dominio simple, la implementación de un controlador REST, el manejo de excepciones de validación y la prueba de nuestros resultados.

1. Dependencias de Maven

Para llevar a cabo esta tarea, comenzaremos configurando las dependencias de nuestro proyecto. Las dependencias son bastante estándar:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <version>2.1.214</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

Para crear un controlador REST que declare un objeto de dominio y lo valide, necesitamos incluir spring-boot-starter-validation, el cual garantiza que Hibernate Validator esté disponible en nuestro proyecto.

2. Una Clase de Dominio Simple

Ahora que tenemos las dependencias configuradas, el siguiente paso es definir una clase de entidad JPA simple que modele a los usuarios. A continuación se muestra un ejemplo de esta clase:

import javax.persistence.*;
import javax.validation.constraints.NotBlank;

@Entity
public class User {
    
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;
    
    @NotBlank(message = "Name is mandatory")
    private String name;
    
    @NotBlank(message = "Email is mandatory")
    private String email;

    // Constructors, Getters, Setters, toString
}

En esta clase del User, empleamos la restricción @NotBlank de la Validación de Beans para asegurarnos de que los campos name y email no sean nulos ni vacíos. Los mensajes de error también se especifican utilizando el atributo message.

Esto significa que cuando Spring Boot valida la clase, los campos restringidos deben ser no nulos y su longitud después de recortar debe ser mayor a cero.

Además, Bean Validation ofrece muchas otras restricciones útiles que podemos aplicar y combinar en nuestras clases restringidas.

3. Implementación de un Controlador REST

A continuación, implementaremos un controlador que nos permitirá recibir los valores asignados a nuestro objeto User, validar los datos y llevar a cabo otras tareas dependiendo de los resultados de la validación. La implementación de un controlador REST es bastante simple:

import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

@RestController
public class UserController {

    @PostMapping("/users")
    ResponseEntity addUser(@Valid @RequestBody User user) {
        // Persistir el usuario
        return ResponseEntity.ok("User is valid");
    }
}

Aquí, utilizamos la anotación @Valid en el método addUser(). Cuando Spring Boot encuentra un argumento anotado con @Valid, automáticamente arranca la implementación predeterminada de JSR 380 — Hibernate Validator — y valida el argumento.

Si el objeto User no pasa la validación, Spring Boot arrojará una excepción MethodArgumentNotValidException.

4. La Anotación @ExceptionHandler

Si bien es útil que Spring Boot valide automáticamente el objeto User, es crucial saber cómo procesar los resultados de la validación. La anotación @ExceptionHandler nos permite manejar tipos específicos de excepciones a través de un único método.

Para procesar los errores de validación, podemos implementar un manejador de excepciones como el siguiente:

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;

@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MethodArgumentNotValidException.class)
public Map<String, String> handleValidationExceptions(MethodArgumentNotValidException ex) {
    Map<String, String> errors = new HashMap<>();
    ex.getBindingResult().getAllErrors().forEach((error) -> {
        String fieldName = ((FieldError) error).getField();
        String errorMessage = error.getDefaultMessage();
        errors.put(fieldName, errorMessage);
    });
    return errors;
}

Este método almacena el nombre y el mensaje de error de cada campo inválido en un Map, que luego se devuelve como representación JSON al cliente para un procesamiento adicional.

5. Probando el Controlador REST

Podemos probar fácilmente la funcionalidad de nuestro controlador REST mediante una prueba de integración. Para ello, comenzamos simulando/inyectando la implementación de la interfaz UserRepository, junto con la instancia del UserController y un objeto MockMvc:

import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@WebMvcTest
@AutoConfigureMockMvc
public class UserControllerIntegrationTest {

    @MockBean
    private UserRepository userRepository;
    
    @Autowired
    UserController userController;

    @Autowired
    private MockMvc mockMvc;

    //...
}

Usamos la anotación @WebMvcTest, que nos permite probar solicitudes y respuestas utilizando métodos estáticos implementados por las clases MockMvcRequestBuilders y MockMvcResultMatchers.

Veamos cómo probar el método addUser() con objetos User válidos e inválidos:

@Test
public void whenPostRequestToUsersAndValidUser_thenCorrectResponse() throws Exception {
    String user = "{"name": "bob", "email" : "bob@example.com"}";
    mockMvc.perform(MockMvcRequestBuilders.post("/users")
      .content(user)
      .contentType(MediaType.APPLICATION_JSON))
      .andExpect(MockMvcResultMatchers.status().isOk())
      .andExpect(MockMvcResultMatchers.content().string("User is valid"));
}

@Test
public void whenPostRequestToUsersAndInvalidUser_thenCorrectResponse() throws Exception {
    String user = "{"name": "", "email" : "bob@example.com"}";
    mockMvc.perform(MockMvcRequestBuilders.post("/users")
      .content(user)
      .contentType(MediaType.APPLICATION_JSON))
      .andExpect(MockMvcResultMatchers.status().isBadRequest())
      .andExpect(MockMvcResultMatchers.jsonPath("$.name", Is.is("Name is mandatory")));
}

6. Ejecutando la Aplicación

Finalmente, podemos ejecutar nuestro proyecto de ejemplo mediante un método estándar main():

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

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

Equipada con un CommandLineRunner, inicializaremos algunos usuarios en nuestra base de datos H2 para proceder a la validación y persistencia.

import org.springframework.context.annotation.Bean;
import org.springframework.boot.CommandLineRunner;

@Bean
public CommandLineRunner run(UserRepository userRepository) throws Exception {
    return (String[] args) -> {
        User user1 = new User("Bob", "bob@example.com");
        User user2 = new User("Jenny", "jenny@example.com");
        userRepository.save(user1);
        userRepository.save(user2);
        userRepository.findAll().forEach(System.out::println);
    };
}

7. Conclusiones Prácticas

En este artículo, hemos aprendido los conceptos básicos de la validación en Spring Boot utilizando Hibernate Validator. Algunas conclusiones y consejos prácticos que se pueden extraer son:

  • Utiliza Anotaciones de Validación: Las anotaciones proporcionan una forma sencilla y efectiva de validar datos de entrada.
  • Manejadores de Excepción: Implementar manejadores de excepción mejora la experiencia del API, permitiendo a los desarrolladores entender rápidamente qué salió mal con una solicitud.
  • Pruebas: No subestimes la importancia de las pruebas de integración para asegurar que tu controlador REST maneje correctamente tanto los casos válidos como los inválidos.
  • Configuración de Base de Datos: Asegúrate de que la configuración de tu base de datos esté alineada con las necesidades de tu aplicación para evitar problemas inesperados.

Con estos consejos prácticos, estarás mejor preparado para manejar la validación de objetos de dominio en tus aplicaciones Spring Boot.