Validação de Parâmetros em Java com Bean Validation API

A validação de parâmetros de entrada em aplicações Java é uma tarefa comum e essencial. Frequentemente, antes de executar a lógica de negócio de um método, é necessário verificar a validade dos argumentos recebidos, como a ausência de valores nulos ou a conformidade com formatos esperados. Tradicionalmente, isso envolvia uma série de verificações condicionais (if-else), o que resultava em código menos legível e elegante. Para padronizar e simplificar esse processo, a Java Community Process (JCP) definiu a Java Validation API (também conhecida como JSR 303).

É importante notar que a Java Validation API define apenas um contrato (uma especificação), e não uma implementação concreta. Diversas bibliotecas fornecem essa implementação. Abaixo, exploramos duas das mais utilizadas.

Implementações da Java Validation API

Hibernate Validator

Esta é uma implementação popular da API de validação, desenvolvida pela equipe do Hibernate. Para validar objetos complexos (POJOs), você pode utilizar a anotação @Valid em conjunto com as anotações de validação padrão. No entanto, o Hibernate Validator puro não permite a aplicação direta de anotações como @NotNull ou @Min em parâmetros individuais de métodos.


<dependency>
   <groupId>org.hibernate.validator</groupId>
   <artifactId>hibernate-validator</artifactId>
   <version>6.0.20.Final</version>
</dependency>
   

Spring Boot Starter Validation

O Spring Boot oferece uma abstração e extensão sobre o Hibernate Validator através do starter spring-boot-starter-validation. Alternativamente, se você já utiliza spring-boot-starter-web, a dependência de validação já está incluída. O Spring introduz a anotação @Validated, que pode ser aplicada tanto a nível de classe quanto para marcar parâmetros de métodos. Sua principal vantagem é o suporte à validação de parâmetros individuais, desde que a anotação @Validated seja aplicada na classe controladora.

A anotação @Validated pode substituir completamente o uso de @Valid, oferecendo maior flexibilidade.


<!-- Se você já usa web starter, a validação já está incluída -->
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- Ou adicione explicitamente -->
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-validation</artifactId>
</dependency>
   

Analisando as dependências, tanto starter-web quanto starter-validation dependem de hibernate-validator, que por sua vez depende da validation-api. Anotações comuns como @NotBlank, @NotNull e @Min são provenientes do pacote javax.validation.constraints da validation-api. O uso de anotações específicas do Hibernate Validator não é mais recomendado em favor das anotações padronizadas pela JSR.

Exemplo de Uso com Grupos de Validação

Considere os seguintes requisitos:

  • Registro de Usuário: Nome, idade e e-mail não podem ser vazios.
  • Atualização de Usuário: Nome, idade, e-mail e ID do usuário não podem ser vazios.
  • Consulta de Usuário: Recebe apenas o ID do usuário como parâmetro individual.

Definição de Grupos de Validação

Para diferenciar as regras de validação entre registro e atualização, podemos usar grupos:


/**
* Grupo de validação para operações de adição.
*/
public interface ValidacaoAdicao {
}
   

/**
* Grupo de validação para operações de atualização.
*/
public interface ValidacaoAtualizacao {
}
   

Objeto de Requisição do Usuário

Definimos o DTO (Data Transfer Object) com as anotações de validação, especificando os grupos quando necessário.


import javax.validation.constraints.Email;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import lombok.Builder;
import lombok.Data;

/**
* Objeto de requisição para operações de usuário.
*/
@Data
@Builder
public class UserRequest implements java.io.Serializable {

   private static final long serialVersionUID = -2655536314774756670L;

   /**
    * Identificador único do usuário.
    */
   @NotNull(message = "ID do usuário não pode ser nulo", groups = {ValidacaoAtualizacao.class})
   @Min(value = 1, message = "ID do usuário deve ser positivo", groups = {ValidacaoAtualizacao.class})
   private Long userId;

   /**
    * Idade do usuário.
    */
   @NotNull(message = "Idade não pode ser nula", groups = {ValidacaoAtualizacao.class, ValidacaoAdicao.class})
   @Min(value = 1, message = "Idade deve ser pelo menos 1", groups = {ValidacaoAtualizacao.class, ValidacaoAdicao.class})
   private Integer age;

   /**
    * Nome do usuário.
    */
   @NotBlank(message = "Nome não pode ser vazio", groups = {ValidacaoAtualizacao.class, ValidacaoAdicao.class})
   private String name;

   /**
    * Endereço de e-mail do usuário.
    */
   @NotBlank(message = "E-mail não pode ser vazio", groups = {ValidacaoAtualizacao.class, ValidacaoAdicao.class})
   @Email(message = "Formato de e-mail inválido")
   private String email;

   /**
    * Apelido do usuário.
    */
   private String nickName;
}
   

Controlador (Controller)

No controlador, usamos @Validated na classe e nas chamadas de método para especificar os grupos de validação e coletar os resultados através de BindingResult.


import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

/**
* Controlador para operações de usuário.
*/
@RestController
@Validated // Habilita a validação de parâmetros individuais nesta classe
public class UserController {

   /**
    * Registra um novo usuário.
    * @param userRequest Dados do usuário a serem registrados.
    * @param bindingResult Resultado da validação.
    * @return Mensagens de erro, se houver.
    */
   @PostMapping("/user/register")
   public String registerUser(@Validated(ValidacaoAdicao.class) @RequestBody UserRequest userRequest, BindingResult bindingResult) {
       return processValidationErrors(bindingResult);
   }

   /**
    * Obtém informações de um usuário por ID.
    * @param userId O ID do usuário a ser buscado.
    * @param bindingResult Resultado da validação.
    * @return Mensagens de erro, se houver.
    */
   @PostMapping("/user/get")
   public String getUser(@NotNull(message = "ID do usuário é obrigatório") @Min(value = 1, message = "ID do usuário deve ser positivo") Long userId, BindingResult bindingResult) {
       // Lógica para obter o usuário seria colocada aqui
       return processValidationErrors(bindingResult);
   }

   /**
    * Atualiza um usuário existente.
    * @param userRequest Dados atualizados do usuário.
    * @param bindingResult Resultado da validação.
    * @return Mensagens de erro, se houver.
    */
   @PostMapping("/user/update")
   public String updateUser(@Validated(ValidacaoAtualizacao.class) @RequestBody UserRequest userRequest, BindingResult bindingResult) {
       return processValidationErrors(bindingResult);
   }

   /**
    * Processa e retorna os erros de validação.
    * @param bindingResult O resultado da validação.
    * @return Uma string contendo as mensagens de erro concatenadas, ou uma string vazia se não houver erros.
    */
   private String processValidationErrors(BindingResult bindingResult) {
       if (bindingResult.hasErrors()) {
           return bindingResult.getFieldErrors().stream()
                   .map(fieldError -> fieldError.getDefaultMessage())
                   .collect(Collectors.joining(", "));
       }
       return ""; // Indica sucesso
   }
}
   

Tratamento Centralizado de Exceções de Validação

A abordagem anterior exige a passagem de BindingResult em cada método do controlador. Para uma solução mais limpa, podemos centralizar o tratamento das exceções de validação usando @RestControllerAdvice.


import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;

import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

/**
* Handler global para exceções de validação e outros erros relacionados a requisições.
*/
@RestControllerAdvice
public class GlobalExceptionHandler {

   /**
    * Trata exceções de validação para argumentos de método não válidos (@RequestBody).
    * @param exception A exceção lançada.
    * @return Uma resposta padronizada com os erros.
    */
   @ExceptionHandler(MethodArgumentNotValidException.class)
   public ApiResponse handleMethodArgumentNotValidException(MethodArgumentNotValidException exception) {
       return formatValidationErrors(exception.getBindingResult());
   }

   /**
    * Trata exceções de quando o tipo do argumento do método não corresponde.
    * @param exception A exceção lançada.
    * @return Uma resposta padronizada com o erro.
    */
   @ExceptionHandler(MethodArgumentTypeMismatchException.class)
   public ApiResponse handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException exception) {
       String errorMessage = String.format("Parâmetro '%s' com valor inválido '%s'. Esperado tipo %s.",
               exception.getName(),
               exception.getValue(),
               exception.getRequiredType().getSimpleName());
       return new ApiResponse(400, errorMessage, null);
   }

   /**
    * Trata exceções de vinculação de parâmetros (geralmente para parâmetros de formulário ou query).
    * @param exception A exceção lançada.
    * @return Uma resposta padronizada com os erros.
    */
   @ExceptionHandler(value = BindException.class)
   public ApiResponse handleBindException(BindException exception) {
       return formatValidationErrors(exception.getBindingResult());
   }

   /**
    * Trata exceções de violação de restrições para parâmetros individuais.
    * Usado quando @Validated é aplicado a parâmetros de método diretamente.
    * @param exception A exceção lançada.
    * @return Uma resposta padronizada com os erros.
    */
   @ExceptionHandler(value = ConstraintViolationException.class)
   public ApiResponse handleConstraintViolationException(ConstraintViolationException exception) {
       return formatConstraintViolations(exception.getConstraintViolations());
   }

   /**
    * Formata os erros de validação de um BindingResult em uma resposta padronizada.
    * @param result O BindingResult contendo os erros.
    * @return Um objeto ApiResponse com as mensagens de erro.
    */
   private ApiResponse formatValidationErrors(BindingResult result) {
       List<string> errors = result.getFieldErrors().stream()
               .map(fieldError -> String.format("%s: %s", fieldError.getField(), fieldError.getDefaultMessage()))
               .collect(Collectors.toList());
       return new ApiResponse(400, "Erros de validação", errors);
   }

   /**
    * Formata um conjunto de ConstraintViolations em uma resposta padronizada.
    * @param violations O conjunto de violações de restrição.
    * @return Um objeto ApiResponse com as mensagens de erro.
    */
   private ApiResponse formatConstraintViolations(Set extends ConstraintViolation> violations) {
       List<string> errors = violations.stream()
               .map(ConstraintViolation::getMessage)
               .collect(Collectors.toList());
       return new ApiResponse(400, "Erros de validação", errors);
   }

   // Assumindo que você tem uma classe base de resposta como esta:
   // public class ApiResponse {
   //     private int status;
   //     private String message;
   //     private Object data;
   //
   //     public ApiResponse(int status, String message, Object data) {
   //         this.status = status;
   //         this.message = message;
   //         this.data = data;
   //     }
   //     // Getters e Setters...
   // }
}
   </string></string>

Com o GlobalExceptionHandler configurado, não é mais necessário passar BindingResult nos métodos do controlador. As exceções de validação serão capturadas e tratadsa automaticamente, retornando uma resposta padronizada ao cliente.

Tags: java Spring Boot bean validation jsr 303 hibernate validator

Publicado em 6-11 07:29 por Thomas