404 Not Found

404 Not Found


nginx

Validação de Dados

A validação de dados é a primeira linha de defesa de uma API—entrada inválida não deve chegar à lógica de negócio, e quanto mais cedo for rejeitada, melhor.

1. O Que Você Vai Aprender


2. Uma História Real de um Desenvolvedor de API

(1) Ponto de Dor: Dados Sujos Sendo Armazenados no Banco de Dados

Alice descobriu alguns dados estranhos no banco de dados do OrderFlow: uma quantidade de pedido de -5, um endereço de email formatado como "abc," e um preço de produto de 0. Bob relatou que um usuário havia enviado um produto com estoque negativo via API, causando anomalias nas estatísticas do relatório. Alice havia escrito muito código de validação if-else na camada de serviço, que era tanto verboso quanto propenso a omissões.

(2) A Solução Bean Validation

Declare regras de validação usando anotações, e o Spring dispara a validação automaticamente:

JAVA
public record CreateOrderRequest(
    @NotNull Long productId,
    @Min(1) @Max(100) Integer quantity,
    @Email String customerEmail
) {}

Requisições inválidas são rejeitadas antes de chegarem ao controller.

(3) Resultado

Alice substituiu todo código de validação manual por Bean Validation, reduzindo a quantidade de código no controller em 40% e garantindo que nenhuma regra de validação seja jamais negligenciada. Não há mais dados sujos no banco de dados.


3. O Sistema de Anotações Bean Validation

(1) Fluxo de Execução de Verificação

100%
graph TD
    A["Requisição do Cliente<br/>@RequestBody"] --> B{"@Valid<br/>Disparado?"}
    B -->|Sim| C["Hibernate Validator<br/>Verificar Restrições"]
    C --> D{"Todos Válidos?"}
    D -->|Sim| E["Método do Controller<br/>Executa"]
    D -->|Não| F["MethodArgumentNotValidException<br/>400 Bad Request"]
    B -->|Não| G["Ignorar Validação<br/>Possíveis dados sujos"]

(3) Método de disparo de verificação

Anotação Finalidade Localização
@Valid Disparar validação em cascata (incluindo objetos aninhados) Parâmetros de método, campos
@Validated Suporta verificação por grupo Classe e parâmetros de método
@Validated(Group.class) Especificar Grupo de Validação Parâmetros de método

(1) ▶ Exemplo: Validação de Parâmetros do Controller

JAVA
@RestController
@RequestMapping("/api/v1/orders")
public class OrderController {

    @PostMapping
    public ResponseEntity<Order> createOrder(
            @Valid @RequestBody CreateOrderRequest request) {
        // Se a validação falhar, MethodArgumentNotValidException é lançada
        // antes de chegar a esta linha
        Order order = orderService.createOrder(request);
        return ResponseEntity.status(HttpStatus.CREATED).body(order);
    }
}

public record CreateOrderRequest(
    @NotNull(message = "Product ID is required")
    Long productId,

    @Min(value = 1, message = "Quantity must be at least 1")
    @Max(value = 100, message = "Quantity cannot exceed 100")
    Integer quantity,

    @Email(message = "Invalid email format")
    String customerEmail
) {}

Saída:

TEXT
// Execução bem-sucedida

4. Anotações de Restrição Comuns

(1) Referência Rápida de Anotações de Restrição

Anotação Tipo Aplicável Descrição Exemplo
@NotNull Todos os tipos Não pode ser null @NotNull Long id
@NotBlank String Não pode ser vazia/null/apenas espaços @NotBlank String name
@NotEmpty String/Coleção Não pode ser vazia/null @NotEmpty List<String> tags
@Size String/Coleção Intervalo de Comprimento/Tamanho @Size(min=2, max=100)
@Min / @Max Tipo Numérico Intervalo de Valor @Min(0) @Max(99999)
@Positive Tipo numérico Número positivo @Positive BigDecimal price
@Email String Formato de Email @Email String email
@Pattern String Correspondência de expressão regular @Pattern(regexp="^[A-Z]")
@Past / @Future Tipo Data Passado/Futuro @Past LocalDate birthDate

(1) ▶ Exemplo: Validação de DTO de Produto

JAVA
public record CreateProductRequest(
    @NotBlank(message = "Product name is required")
    @Size(min = 2, max = 200, message = "Name must be 2-200 characters")
    String name,

    @NotNull(message = "Price is required")
    @Positive(message = "Price must be positive")
    @DecimalMin(value = "0.01", message = "Price must be at least 0.01")
    BigDecimal price,

    @NotNull(message = "Stock is required")
    @Min(value = 0, message = "Stock cannot be negative")
    Integer stock,

    @Email(message = "Supplier email must be valid")
    String supplierEmail,

    @Pattern(regexp = "^[A-Z]{3}-\\d{4}$", message = "SKU format: XXX-0000")
    String sku
) {}

Saída:

TEXT
// Execução bem-sucedida
Comparação de Anotações null "" " " "abc"
@NotNull
@NotBlank
@NotEmpty
🔥 Erro Comum: Ao validar o tipo String, use @NotBlank em vez de @NotNull, porque uma string vazia geralmente também é inválida.


5. Validadores Personalizados

(1) Passos de Implementação

  1. Definir Anotação de Restrição
  2. Implementar ConstraintValidator<A, T>
  3. Usar em campos DTO

(1) ▶ Exemplo: Validador personalizado @ValidOrderQuantity

JAVA
// Passo 1: Definir anotação de restrição
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = OrderQuantityValidator.class)
public @interface ValidOrderQuantity {
    String message() default "Order quantity exceeds product stock limit";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

// Passo 2: Implementar ConstraintValidator
public class OrderQuantityValidator
        implements ConstraintValidator<ValidOrderQuantity, Integer> {

    private static final int MAX_QUANTITY_PER_ITEM = 100;

    @Override
    public boolean isValid(Integer quantity, ConstraintValidatorContext context) {
        if (quantity == null) {
            return true; // Deixar @NotNull lidar com verificação de null
        }
        return quantity >= 1 && quantity <= MAX_QUANTITY_PER_ITEM;
    }
}

// Passo 3: Usar no DTO
public record CreateOrderRequest(
    @NotNull Long productId,
    @ValidOrderQuantity Integer quantity
) {}

Saída:

TEXT
// Execução bem-sucedida

6. Validação por Grupo

(1) Categorizando Regras de Validação por Cenário

Os cenários "Criar" e "Atualizar" normalmente requerem regras de validação diferentes:

Cenário ID do Produto Nome Preço
Criar Gerado automaticamente; não obrigatório Obrigatório Obrigatório
Atualizar Obrigatório (para indicar quem fez a alteração) Opcional Opcional

(1) ▶ Exemplo: Validação por Grupo

JAVA
// Definir interfaces de grupo
public interface Create {}
public interface Update {}

// DTO com validação consciente de grupo
public record ProductRequest(
    @Null(groups = Create.class, message = "ID must be null for creation")
    @NotNull(groups = Update.class, message = "ID is required for update")
    Long id,

    @NotBlank(groups = Create.class, message = "Name is required for creation")
    @Size(min = 2, max = 200)
    String name,

    @NotNull(groups = Create.class, message = "Price is required for creation")
    @Positive BigDecimal price
) {}

Saída:

TEXT
// Execução bem-sucedida
JAVA
@RestController
@RequestMapping("/api/v1/products")
public class ProductController {

    @PostMapping
    public ResponseEntity<Product> create(
            @Validated(Create.class) @RequestBody ProductRequest request) {
        // Apenas validações do grupo Create são aplicadas
        // ...
        return ResponseEntity.status(HttpStatus.CREATED).build();
    }

    @PutMapping("/{id}")
    public ResponseEntity<Product> update(
            @PathVariable Long id,
            @Validated(Update.class) @RequestBody ProductRequest request) {
        // Apenas validações do grupo Update são aplicadas
        // ...
        return ResponseEntity.ok().build();
    }
}

(2) ▶ Exemplo: Validação Aninhada

JAVA
public record CreateOrderRequest(
    @NotNull Long productId,
    @ValidOrderQuantity Integer quantity,
    @Valid @NotNull ShippingAddress shippingAddress
) {}

public record ShippingAddress(
    @NotBlank String street,
    @NotBlank String city,
    @NotBlank String zipCode,
    @Pattern(regexp = "^[A-Z]{2}$") String country
) {}

Saída:

TEXT
// Execução bem-sucedida
📌 Ponto-Chave: Objetos aninhados devem incluir @Valid; caso contrário, a validação de campos aninhados não entrará em vigor. @Validated não suporta validação em cascata aninhada.


7. Formatando Respostas de Erro de Validação

(1) Padronizar o formato de resposta de erro

JAVA
@RestControllerAdvice
public class ValidationExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Map<String, Object>> handleValidation(
            MethodArgumentNotValidException ex) {
        Map<String, Object> body = new LinkedHashMap<>();
        body.put("timestamp", Instant.now());
        body.put("status", HttpStatus.BAD_REQUEST.value());

        List<Map<String, String>> errors = ex.getBindingResult()
            .getFieldErrors().stream()
            .map(fe -> Map.of(
                "field", fe.getField(),
                "message", fe.getDefaultMessage() != null ? fe.getDefaultMessage() : "",
                "rejectedValue", fe.getRejectedValue() != null ? fe.getRejectedValue().toString() : "null"
            ))
            .toList();
        body.put("errors", errors);
        return ResponseEntity.badRequest().body(body);
    }
}
💻 Saída:

JSON
{
  "timestamp": "2024-01-15T10:00:00Z",
  "status": 400,
  "errors": [
    {"field": "quantity", "message": "Quantity must be at least 1", "rejectedValue": "0"},
    {"field": "customerEmail", "message": "Invalid email format", "rejectedValue": "abc"}
  ]
}

8. Exemplo Abrangente: O Sistema de Verificação Completo do OrderFlow

JAVA
// Grupos de validação
package com.orderflow.validation;
public interface Create {}
public interface Update {}

// Validador personalizado: @ValidShippingAddress
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = ShippingAddressValidator.class)
public @interface ValidShippingAddress {
    String message() default "Invalid shipping address";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

public class ShippingAddressValidator
        implements ConstraintValidator<ValidShippingAddress, String> {
    @Override
    public boolean isValid(String address, ConstraintValidatorContext ctx) {
        if (address == null || address.isBlank()) return false;
        return address.length() >= 10 && address.length() <= 500;
    }
}

// DTOs
public record CreateOrderRequest(
    @NotNull(groups = Create.class) Long productId,
    @Min(value = 1, message = "Quantity must be at least 1")
    @Max(value = 100, message = "Quantity cannot exceed 100")
    Integer quantity,
    @Email String customerEmail,
    @ValidShippingAddress String shippingAddress
) {}

public record CreateProductRequest(
    @Null(groups = Create.class) @NotNull(groups = Update.class) Long id,
    @NotBlank(groups = Create.class) @Size(min = 2, max = 200) String name,
    @NotNull(groups = Create.class) @Positive BigDecimal price,
    @Min(0) Integer stock,
    @Pattern(regexp = "^[A-Z]{3}-\\d{4}$", message = "SKU: XXX-0000") String sku
) {}

// Controller
@RestController
@RequestMapping("/api/v1/orders")
public class OrderController {
    @PostMapping
    public ResponseEntity<Void> create(
            @Validated(Create.class) @RequestBody CreateOrderRequest req) {
        // Validação passou, prosseguir para lógica de negócio
        return ResponseEntity.status(HttpStatus.CREATED).build();
    }
}

❓ Perguntas Frequentes

P Qual é a diferença entre @Valid e @Validated?
R @Valid é uma anotação padrão JSR-380 que suporta validação aninhada e em cascata. @Validated é uma anotação de extensão Spring que suporta validação agrupada. Use @Validated para agrupamento e @Valid para aninhamento; os dois podem ser usados em combinação.
P O que é retornado quando a validação falha?
R Por padrão, o Spring Boot retorna um código de status 400 Bad Request junto com uma mensagem de erro JSON. Você pode personalizar o formato usando @RestControllerAdvice; esta aula fornece uma solução de formatação padronizada.
P Como escolher entre @NotBlank, @NotEmpty e @NotNull?
R Use @NotBlank para Strings (não permite null, strings vazias ou strings consistindo apenas de espaços); use @NotEmpty para Collections (não permite null ou coleções vazias); e use @NotNull para todos os outros tipos.
P Validação por grupo e validação padrão podem ser habilitadas ao mesmo tempo?
R Por padrão, o grupo "Default" é desabilitado uma vez que um grupo específico é especificado. Se você quiser que ambos sejam habilitados, faça o grupo personalizado herdar de "Default": public interface Create extends Default {}.
P Spring Beans podem ser injetados em validadores personalizados?
R Sim. ConstraintValidators são gerenciados pelo container Spring, então você pode usar @Autowired para injetar Beans no método isValid (por exemplo, para consultar o banco de dados para validação de unicidade).
P Como internacionalizar mensagens de erro?
R Defina chaves de mensagem em resources/ValidationMessages.properties, como order.quantity.invalid=Order quantity must be between {min} and {max}; arquivos multilíngues são suportados.

📖 Resumo


📝 Exercícios

  1. Exercício Básico (Dificuldade: ⭐): Adicione anotações Bean Validation ao CreateOrderRequest e CreateProductRequest do OrderFlow para validar entrada inválida e retornar um erro 400.

  2. Exercício Avançado (Dificuldade ⭐⭐): Implemente validação agrupada—para operações Create, name e price são obrigatórios; para operações Update, id é obrigatório e name e price são opcionais. Implemente um validador @ValidShippingAddress personalizado.

  3. Desafio (Dificuldade: ⭐⭐⭐): Crie um validador @UniqueProductSku que injete o ProductRepository para verificar se um SKU já existe, implementando validação de unicidade em nível de banco de dados. Considere o limite entre as responsabilidades do validador e da camada de negócio.

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%