Validação no Spring Boot com Bean Validation (@Valid, @NotBlank, …)
1. Visão geral
Ao construir uma REST API, você sempre precisa validar os dados de entrada antes de processar a lógica de negócio: email em formato correto, nome não pode estar vazio, idade deve ser positiva… Bean Validation (JSR 380, implementado por Hibernate Validator) ajuda você a fazer isso com annotations, em vez de escrever centenas de if-else manuais.
Este artigo passa por como configurar, usar @Valid junto com as annotations mais comuns, validar objetos aninhados, escrever validadores customizados e tratar erros de validação para retornar uma resposta amigável ao cliente. Depois de ler, você estará confiante em aplicar validação a todos os DTOs em seu projeto Spring Boot.
2. Adicionar dependência
Spring Boot não vem com Bean Validation por padrão, você precisa adicionar spring-boot-starter-validation:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
Esta dependência traz Hibernate Validator – a implementação mais comum da Bean Validation API. Se você usa spring-boot-starter-web, muitos pensam que validation já vem incluída, mas na verdade desde Spring Boot 2.3, este starter foi separado – sem ele, @Valid não funciona e nenhum erro é reportado, dados inválidos passam direto.
3. Validar corpo da requisição com @Valid
3.1. Declarar DTO com constraint annotations
public record CreateOrderRequest(
@NotBlank(message = "Nome do cliente não pode estar vazio")
String customerName,
@Email(message = "Email inválido")
@NotBlank
String email,
@Positive(message = "Quantidade deve ser maior que 0")
Integer quantity,
@DecimalMin(value = "0.0", inclusive = false, message = "Preço deve ser maior que 0")
BigDecimal unitPrice
) {}
Cada campo recebe uma ou mais constraint annotations: @NotBlank rejeita null/vazio/apenas-espaços, @Email valida formato de email, @Positive exige número positivo, @DecimalMin estabelece valor mínimo. O parâmetro message é o texto do erro retornado ao violar a constraint.
3.2. Aplicar @Valid no controller
@RestController
@RequestMapping("/api/orders")
public class OrderController {
@PostMapping
public ResponseEntity<String> createOrder(@Valid @RequestBody CreateOrderRequest request) {
// request garantidamente está válido neste ponto
return ResponseEntity.ok("Pedido criado para " + request.customerName());
}
}
@Valid antes de @RequestBody avisa ao Spring MVC para executar validação no objeto que foi deserializado do JSON. Se algum field violar uma constraint, Spring lança MethodArgumentNotValidException antes do body do método executar – isso significa que você não precisa verificar null ou vazio na lógica de negócio.
Enviar uma requisição sem customerName:
POST /api/orders
{
"email": "a@b.com",
"quantity": 2,
"unitPrice": 10.5
}
Por padrão (sem tratamento de exceção), Spring retorna HTTP 400 com um body longo e difícil de ler para o cliente. A seção 5 mostrará como tratar isso de forma mais elegante.
4. Annotations mais comuns
| Annotation | Significado |
|---|---|
@NotNull |
Valor não pode ser null |
@NotBlank |
String não null, não vazio, não apenas espaços |
@NotEmpty |
Collection/String/Array não null e não vazio |
@Size(min, max) |
Comprimento de String ou tamanho de Collection |
@Min / @Max |
Limites de números inteiros |
@Positive / @Negative |
Número positivo/negativo |
@Email |
Formato de email válido |
@Pattern(regexp) |
Deve corresponder a uma regex |
@Past / @Future |
Data no passado/futuro |
Exemplo combinando múltiplas constraints em um field:
public class Customer {
@Size(min = 3, max = 50, message = "Nome deve ter de 3 a 50 caracteres")
private String name;
@Pattern(regexp = "^0\\d{9}$", message = "Telefone deve ter 10 dígitos, começando com 0")
private String phone;
@Past(message = "Data de nascimento deve ser no passado")
private LocalDate birthDate;
}
Você pode aplicar várias annotations no mesmo field – por exemplo, @NotBlank combinado com @Size – para rejeitar vazio e limitar comprimento. O validador executa todas as constraints e agrega erros, não para na primeira falha.
5. Tratar erros de validação com @ExceptionHandler
Em vez de deixar Spring retornar a resposta padrão confusa, use @RestControllerAdvice para formatar melhor:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidationErrors(
MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
for (FieldError error : ex.getBindingResult().getFieldErrors()) {
errors.put(error.getField(), error.getDefaultMessage());
}
return ResponseEntity.badRequest().body(errors);
}
}
MethodArgumentNotValidException contém BindingResult com uma lista de FieldError – cada erro tem nome do field e mensagem. O código acima agrega todos os erros em um Map<field, message> e retorna HTTP 400.
Resultado ao chamar novamente a requisição sem customerName:
HTTP/1.1 400 Bad Request
Content-Type: application/json
{
"customerName": "Nome do cliente não pode estar vazio"
}
A resposta agora é concisa, o cliente consegue mapear erros para cada field no formulário.
6. Validar objetos aninhados e collections
6.1. Objeto aninhado com @Valid
public record AddressRequest(
@NotBlank String city,
@NotBlank String street
) {}
public record CustomerRequest(
@NotBlank String name,
@Valid
@NotNull
AddressRequest address
) {}
Se CustomerRequest.address tiver apenas @NotNull sem @Valid, as constraints dentro de AddressRequest (como @NotBlank em city) não serão verificadas – este é um erro muito comum com iniciantes em Bean Validation. @Valid aqui pede ao validador para “descer” no objeto aninhado.
6.2. Validar cada elemento em uma List
public record BulkOrderRequest(
@NotEmpty(message = "Lista de items não pode estar vazia")
@Valid
List<@Valid OrderItem> items
) {}
public record OrderItem(
@NotBlank String productCode,
@Positive Integer quantity
) {}
@Valid colocado antes de List<...> (ou no type argument) avisa ao validador para verificar cada elemento da lista, não apenas se a list é null ou vazia. Se o item 2 da lista tiver quantity = -1, o erro retornado indicará items[1].quantity.
7. Escrever validador customizado
Quando constraints já existentes não são suficientes, crie uma annotation própria. Por exemplo, validar que código de pedido começa com ORD-:
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = OrderCodeValidator.class)
public @interface ValidOrderCode {
String message() default "Código do pedido deve ser ORD-xxxxx";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
public class OrderCodeValidator implements ConstraintValidator<ValidOrderCode, String> {
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null) {
return true; // deixe @NotNull lidar com isso, evite erro duplicado
}
return value.matches("^ORD-\\d{5}$");
}
}
@Constraint(validatedBy = ...) vincula a annotation com a classe que implementa a lógica. ConstraintValidator<A, T> recebe A como a annotation e T como o tipo do field. O método isValid retorna true se válido. Nota: deixe isValid retornar true quando value == null, porque rejeitar null deve ser responsabilidade de @NotNull – assim um field não reporta dois erros diferentes.
Usar no DTO:
public record OrderLookupRequest(
@NotBlank
@ValidOrderCode
String orderCode
) {}
8. Testar validação com JUnit 5 + AssertJ
Você pode testar Validator diretamente sem iniciar o contexto Spring MVC, mais rápido que testes de integração:
class CreateOrderRequestValidationTest {
private static Validator validator;
@BeforeAll
static void setUp() {
try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) {
validator = factory.getValidator();
}
}
@Test
void shouldRejectBlankCustomerName() {
CreateOrderRequest request = new CreateOrderRequest(
"", "a@b.com", 2, BigDecimal.TEN);
Set<ConstraintViolation<CreateOrderRequest>> violations = validator.validate(request);
assertThat(violations)
.extracting(v -> v.getPropertyPath().toString())
.contains("customerName");
}
@Test
void shouldPassWhenAllFieldsValid() {
CreateOrderRequest request = new CreateOrderRequest(
"Nguyen Van A", "a@b.com", 2, BigDecimal.TEN);
Set<ConstraintViolation<CreateOrderRequest>> violations = validator.validate(request);
assertThat(violations).isEmpty();
}
}
Validation.buildDefaultValidatorFactory() cria um Validator independente, sem necessidade de contexto Spring. validator.validate(request) retorna Set<ConstraintViolation> – vazio significa válido. Esta abordagem é ideal para unit tests isolados do DTO, separado da lógica do controller.
9. Validar path variable e query param
@Valid em @RequestBody é o caso mais comum, mas você também pode validar parâmetros individuais adicionando @Validated na classe controller:
@RestController
@RequestMapping("/api/orders")
@Validated
public class OrderQueryController {
@GetMapping("/{id}")
public ResponseEntity<String> getOrder(
@PathVariable @Positive(message = "ID deve ser maior que 0") Long id) {
return ResponseEntity.ok("Pedido " + id);
}
}
@Validated (do Spring, diferente de @Valid do Bean Validation) ativa validação em parâmetros de método. Quando id = -1, Spring lança ConstraintViolationException (diferente de MethodArgumentNotValidException da seção 5) – você precisa de um @ExceptionHandler separado para esta exceção:
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<String> handleConstraintViolation(ConstraintViolationException ex) {
return ResponseEntity.badRequest().body(ex.getMessage());
}
Atenção: Estas duas exceções são fáceis de confundir –
MethodArgumentNotValidExceptioné para@Valid @RequestBody/@ModelAttribute, enquantoConstraintViolationExceptioné para@Validatedem path variable/query param. Sem um dos handlers, o erro cairá no response 500 padrão, muito ruim.
10. Erros comuns e quando considerar alternativas
- Esquecer
@Valid: constraint annotations não se ativam sozinhas, você deve declarar@Validno local correto (parâmetro do controller, field de objeto aninhado, type argument de generic). - Faltar dependency starter-validation: constraint annotations são ignoradas silenciosamente, nenhuma exceção é lançada – muito difícil detectar sem testes.
- Validar lógica complexa envolvendo múltiplos fields (ex: “data de término deve ser após data de início”): Bean Validation consegue fazer via class-level constraint, mas se a lógica depende de dados do banco de dados (ex: “email ainda não existe”), melhor tratar na camada de service em vez de no validador – validadores não devem chamar repository, fica difícil testar e viola princípio da responsabilidade única.
- Mensagens de erro multi-idioma: use
message = "{order.code.invalid}"com arquivomessages.propertiesem vez de hard-code português na annotation, se o projeto precisar de i18n.
11. Conclusão
Bean Validation permite declarar regras de validação direto no DTO via annotations, substituindo if-else espalhado pelo código. Combinando @Valid/@Validated com @RestControllerAdvice, você tem um fluxo de validação – reportagem de erros – consistente para toda a API. Quando constraints pré-definidas não são suficientes, validadores customizados permitem estender de forma flexível mantendo o estilo declarativo, fácil de ler e testar.