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
- Anotação
@Valid/@Validatede Mecanismo de Disparo de Verificação - Anotações de restrição comuns:
@NotNull/@Size/@Pattern/@Email/@Min/@Max - Implementação de Validador Personalizado
ConstraintValidator<A, T> - Validação por Grupo
groupsDistinguir regras de validação por cenário (Create vs. Update) - Formatação uniforme de respostas de erro de validação
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:
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
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
@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:
// 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
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:
// Execução bem-sucedida
| Comparação de Anotações | null | "" | " " | "abc" |
|---|---|---|---|---|
@NotNull |
❌ | ✅ | ✅ | ✅ |
@NotBlank |
❌ | ❌ | ❌ | ✅ |
@NotEmpty |
❌ | ❌ | ✅ | ✅ |
@NotBlank em vez de @NotNull, porque uma string vazia geralmente também é inválida.
5. Validadores Personalizados
(1) Passos de Implementação
- Definir Anotação de Restrição
- Implementar
ConstraintValidator<A, T> - Usar em campos DTO
(1) ▶ Exemplo: Validador personalizado @ValidOrderQuantity
// 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:
// 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
// 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:
// Execução bem-sucedida
@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
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:
// Execução bem-sucedida
@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
@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);
}
}
{
"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
// 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
public interface Create extends Default {}.resources/ValidationMessages.properties, como order.quantity.invalid=Order quantity must be between {min} and {max}; arquivos multilíngues são suportados.📖 Resumo
@Valid/@Validateddispara validação; se a validação falhar,MethodArgumentNotValidExceptioné lançada- Anotações comuns: Use
@NotBlankpara strings,@Min/@Max/@Positivepara números, e@Emailpara endereços de email - Três Passos para Criar um Validador Personalizado: Definir uma anotação → Implementar ConstraintValidator → Usá-la
- Validação por grupo distingue entre cenários Create e Update;
@Validated(Group.class)especifica o grupo - Objetos aninhados devem ter
@Validadicionado para passar por validação em cascata @RestControllerAdviceFormatação Padronizada de Respostas de Erro
📝 Exercícios
-
Exercício Básico (Dificuldade: ⭐): Adicione anotações Bean Validation ao
CreateOrderRequesteCreateProductRequestdo OrderFlow para validar entrada inválida e retornar um erro 400. -
Exercício Avançado (Dificuldade ⭐⭐): Implemente validação agrupada—para operações
Create,nameepricesão obrigatórios; para operaçõesUpdate,idé obrigatório enameepricesão opcionais. Implemente um validador@ValidShippingAddresspersonalizado. -
Desafio (Dificuldade: ⭐⭐⭐): Crie um validador
@UniqueProductSkuque injete oProductRepositorypara 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.



