Validação de Dados em Aplicações Spring Boot

A validação de dados é um aspecto crucial no desenvolvimento de aplicações robustas e seguras. Em ambientes Spring Boot, a validação pode ser implementada de diversas formas, desde a verificação de parâmetros individuais até a validação de objetos aninhados e a centralização do tratamento de erros.

Validação de Objetos Aninhados

Ao trabalhar com objetos de dados que contêm outros objetos como atributos, pode ser necessário validar também os campos dos objetos internos. Para habilitar essa validação em cascata, é preciso adicionar a anotação @Valid ao atributo do objeto aninhado na classe pai.

import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Min;

public class UserRegistrationForm {
    private String username;

    @Valid // Habilita a validação para o objeto AddressDetails
    private AddressDetails shippingAddress;

    // Getters e Setters
}

public class AddressDetails {
    @NotBlank(message = "A rua não pode estar vazia.")
    private String street;

    @NotNull(message = "O número não pode ser nulo.")
    @Min(value = 1, message = "O número deve ser positivo.")
    private Integer number;

    // Getters e Setters
}

Validação de Parâmetros de Métodos

Para validar parâmetros que não são parte de um objeto DTO (Data Transfer Object) em métodos de controllers ou serviços, como strings ou números passados diretamente, a anotação @Validated deve ser aplicada na própria classe do componente (por exemplo, um @RestController). Sem essa anotação no nível da classe, as anotações de validação em parâmetros de métodos simples não serão ativadas.

import jakarta.validation.constraints.NotBlank;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/products")
@Validated // Necessário para validar parâmetros de método
public class ProductManagementController {

    @PostMapping("/find-by-code")
    public void getProductByCode(@NotBlank(message = "O código do produto não pode ser vazio.") String productCode) {
        // Lógica para encontrar o produto
        System.out.println("Buscando produto com código: " + productCode);
    }
}

Diferenças entre @Valid e @Validated

Embora ambas as anotações sejam usadas para validação, elas possuem distinções importantes:

  • @Valid é uma anotação padrão do Bean Validation API (JSR 380/303) e é usada para marcar um objeto para validação recursiva ou para validar um parâmetro simples. Ela não suporta grupos de validação.
  • @Validated é uma anotação específica do Spring. Ela não apenas permite a validação em cascata, mas também oferece suporte a grupos de validação, que permitem especificar quais restrições devem ser aplicadas em diferentes cenários (por exemplo, "CREATE" ou "UPDATE"). Além disso, @Validated é necessária na classe para ativar a validação em parâmetros de métodos simples.

Capturando Resultados de Validação com BindingResult

Quando a validação falha, o Spring lança uma exceção. Para interceptar os detalhes dos erros de validação e fornecer uma resposta mais controlada ao cliente, podemos utilizar a interface BindingResult.

import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;

public class UserDataDTO {
    @NotBlank(message = "O nome de usuário é obrigatório.")
    private String username;

    @NotNull(message = "A idade é obrigatória.")
    @Min(value = 18, message = "A idade mínima é 18 anos.")
    @Max(value = 60, message = "A idade máxima é 60 anos.")
    private Integer age;

    @Pattern(regexp = "^\\d{10,11}$", message = "Número de telefone inválido.")
    private String contactPhone;

    // Getters e Setters
    public String getUsername() { return username; }
    public void setUsername(String username) { this.username = username; }
    public Integer getAge() { return age; }
    public void setAge(Integer age) { this.age = age; }
    public String getContactPhone() { return contactPhone; }
    public void setContactPhone(String contactPhone) { this.contactPhone = contactPhone; }
}

Opção 1: Tratamento Direto com BindingResult

Uma abordagem é injetar BindingResult diretamente no método do controller, logo após o objeto que está sendo validado. Isso permite inspecionar os erros e construir uma resposta personalizada imediatamente.

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.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/validation-examples")
public class ValidationShowcaseController {

    @PostMapping("/submit-user-data")
    public APIResponse processUserData(@RequestBody @Validated UserDataDTO userData, BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            String errorMessage = bindingResult.getFieldError().getDefaultMessage();
            return APIResponse.failure(400, "Erro de validação: " + errorMessage);
        }

        // Lógica de negócio se a validação for bem-sucedida
        System.out.println("Dados do usuário válidos: " + userData.getUsername());
        return APIResponse.success("Dados recebidos com sucesso!");
    }
}

// Classe de resposta simplificada para exemplo
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;
    }

    public static APIResponse success(Object data) {
        return new APIResponse(200, "Sucesso", data);
    }

    public static APIResponse failure(int status, String message) {
        return new APIResponse(status, message, null);
    }

    // Getters
    public int getStatus() { return status; }
    public String getMessage() { return message; }
    public Object getData() { return data; }
}

Opção 2: Tratamento Unificado de Exceções

Para evitar repetir o código de tratamento de erros em cada método do controller, é uma prática comum implementar um handler de exceções global. O Spring lançará MethodArgumentNotValidException para falhas de validação de @RequestBody e BindException para falhass de validação de @ModelAttribute ou parâmetros de métodos simples. Podemos capturar essas exceções e retornar uma resposta padronizada.

import org.springframework.http.HttpStatus;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class GlobalValidationErrorHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public APIResponse handleMethodArgumentNotValid(MethodArgumentNotValidException ex) {
        FieldError fieldError = ex.getBindingResult().getFieldError();
        String errorMessage = (fieldError != null) ? fieldError.getDefaultMessage() : "Erro de validação";
        String fieldName = (fieldError != null) ? fieldError.getField() : "N/A";
        return APIResponse.failure(AppErrorCodes.INVALID_INPUT.getCode(),
                                   AppErrorCodes.INVALID_INPUT.getMessage() + ": Campo '" + fieldName + "' - " + errorMessage);
    }

    @ExceptionHandler(BindException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public APIResponse handleBindException(BindException ex) {
        FieldError fieldError = ex.getBindingResult().getFieldError();
        String errorMessage = (fieldError != null) ? fieldError.getDefaultMessage() : "Erro de vinculação";
        String fieldName = (fieldError != null) ? fieldError.getField() : "N/A";
        return APIResponse.failure(AppErrorCodes.INVALID_INPUT.getCode(),
                                   AppErrorCodes.INVALID_INPUT.getMessage() + ": Campo '" + fieldName + "' - " + errorMessage);
    }
}

// Enum de códigos de erro
enum AppErrorCodes {
    SUCCESS(200, "Operação bem-sucedida"),
    INVALID_INPUT(400, "Dados de entrada inválidos"),
    SERVER_ERROR(500, "Erro interno do servidor");

    private final int code;
    private final String message;

    AppErrorCodes(int code, String message) {
        this.code = code;
        this.message = message;
    }

    public int getCode() {
        return code;
    }

    public String getMessage() {
        return message;
    }
}

Aproveitando a Validação JSR-303/380

O Spring Boot integra-se perfeitamente com a especificação JSR-303 (ou JSR-380, Bean Validation 2.0). Isso permite usar anotações como @Email, @Size, @Pattern, entre outras, diretamente nos campos das classes para aplicar restrições de validação. Para que essas anotações funcionem, especialmente em classes de configuração ou componentes fora de DTOs de requisição, é fundamental que a classe seja marcada com @Validated.

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import org.springframework.validation.annotation.Validated;

@Component
@ConfigurationProperties(prefix = "app.admin")
@Validated // Habilita a validação para os campos desta classe de configuração
public class AdminConfigProperties {

    @Email(message = "O e-mail do administrador deve ser um formato válido.")
    @NotBlank(message = "O e-mail do administrador é obrigatório.")
    private String primaryContactEmail;

    // Getters e Setters
    public String getPrimaryContactEmail() { return primaryContactEmail; }
    public void setPrimaryContactEmail(String primaryContactEmail) { this.primaryContactEmail = primaryContactEmail; }
}

Tags: Spring Boot Validação de Dados bean validation JSR-303 JSR-380

Publicado em 7-12 04:58