Três perguntas diferentes
Primeiro pergunta se a mensagem pode ser lida. JSON mal formado pode falhar antes de existir um DTO. Depois pergunta se os valores cumprem restrições como Min e NotBlank. Finalmente verifica regras de negócio dependentes do contexto, como capacidade disponível, e as permissões do utilizador.
Duas rejeições podem partilhar 400 e precisar de correções diferentes. Um erro de leitura exige corrigir a representação; uma quantidade fora do intervalo exige corrigir o valor. Um DTO válido não prova autorização nem disponibilidade atual. Regista a etapa e um motivo seguro, sem reduzir tudo ao estado HTTP.
A assinatura define o nível
Valid pede validação das restrições do objeto; não é por si uma restrição de valor. Com apenas um DTO Valid e sem BindingResult, a validação individual pode produzir MethodArgumentNotValidException. Acrescentar uma restrição direta, como Min num parâmetro, requer validação do método e pode produzir HandlerMethodValidationException.
Estas aulas usam o suporte integrado do MVC, com provider disponível e sem Validated na classe do controller. Validated ao nível da classe pode envolver validação por proxy e mudar o mecanismo. Ao migrar, revê assinatura, configuração e handlers em conjunto; o nome da anotação isolada não descreve toda a fronteira.
Tratamento local preserva responsabilidade
Um BindingResult imediatamente depois do argumento validado permite tratar os erros no próprio método. Isso torna possível escolher uma resposta local, mas também permite um erro de implementação: chamar o serviço antes de consultar hasErrors. A chegada ao método deixa de ser prova de aceitação das restrições.
Na reserva ilustrativa, units=0 e errors.hasErrors()=true devem conduzir à rejeição definida antes de reserve. Não retires a restrição para esconder a falha. Se existirem outros parâmetros com restrições e erros sem o respetivo Errors adjacente, a validação do método ainda pode impedir a chamada.
Um contrato de erro público
ProblemDetail representa um problema HTTP com campos normativos e extensões. Quando devolvido diretamente pelo handler, o status define o estado da resposta. Um errorCode estável pode permitir ao cliente distinguir um conflito de capacidade sem interpretar frases livres; a extensão deve ser documentada e não contradizer os campos normativos.
Escolhe detalhes que ajudem a corrigir o pedido sem copiar stack traces, SQL ou segredos. Com Jackson, as propriedades próprias são apresentadas como campos JSON. Esse formato não implementa retentativas nem decide se uma nova tentativa é segura. O cliente continua a precisar de uma política explícita.
Âmbito e precedência dos handlers
RestControllerAdvice partilha tratamento com corpo de resposta entre controllers e pode restringir o âmbito por seletores. Um handler local correspondente é considerado antes dos handlers globais. Se um controller devolve 200 para o mesmo conflito que os restantes representam como 409, verifica primeiro o seu tratamento local.
A resolução MVC assenta no DispatcherServlet. Uma exceção num filtro antes de o chamar não entra automaticamente no mesmo percurso. Rever a fronteira onde o erro nasce é mais útil do que aumentar indiscriminadamente a prioridade do advice. Segurança e infraestrutura podem precisar de tratamento próprio, alinhado com o contrato público.
Três resultados do mesmo modelo
Lê o exemplo com provider de validação e conversor JSON disponíveis. units=0 viola Min(1), por isso o corpo do método não é chamado. units=3 passa e produz eligible-in-model. units=6 também passa as restrições 1..20, mas excede a capacidade fictícia 4 e lança CapacityConflictException; o advice devolve 409.
O modelo não consulta stock nem reserva unidades. A capacidade é uma premissa de ensino fixa, não uma medição de produção. Para padronizar também erros MVC incorporados como ProblemDetail no Boot, a propriedade spring.mvc.problemdetails.enabled pode ativar esse suporte; o advice mostrado só trata o conflito próprio.
Rever o contrato completo
Uma revisão útil compara pedidos legíveis e ilegíveis, valores dentro e fora das restrições e um conflito de negócio. Para cada um, identifica se o método foi chamado, se podia produzir efeitos, qual o estado esperado e quais os campos públicos. Inclui uma assinatura com restrição direta para não esquecer a segunda exceção de validação.
As perguntas e casos pedem escolher essas interpretações a partir de evidência ilustrativa; não exigem executar Java. Ao aceitar uma alteração real, evita afirmar que o código está correto apenas porque as respostas têm JSON: formato, significado do estado, âmbito dos handlers e efeitos são dimensões distintas.
// Separate files in package com.acme.shipping; inside the application scan root.
// Compatible MVC JSON support and Jakarta Validation provider are prerequisites.
// ReservationPreviewController.java
package com.acme.shipping;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ReservationPreviewController {
public record Input(@NotBlank String sku, @Min(1) @Max(20) int units) {}
public record Preview(String sku, int units, String outcome) {}
@PostMapping(path = "/reservations/preview", consumes = "application/json",
produces = "application/json")
public Preview preview(@Valid @RequestBody Input input) {
if (input.units() > 4) throw new CapacityConflictException();
return new Preview(input.sku(), input.units(), "eligible-in-model");
}
}
// CapacityConflictException.java
package com.acme.shipping;
public class CapacityConflictException extends RuntimeException {}
// ReservationErrors.java
package com.acme.shipping;
import org.springframework.http.HttpStatus;
import org.springframework.http.ProblemDetail;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice(assignableTypes = ReservationPreviewController.class)
public class ReservationErrors {
@ExceptionHandler(CapacityConflictException.class)
public ProblemDetail capacityConflict() {
ProblemDetail problem = ProblemDetail.forStatusAndDetail(
HttpStatus.CONFLICT, "Requested units exceed this model's capacity.");
problem.setTitle("Capacity conflict");
problem.setProperty("errorCode", "CAPACITY_CONFLICT");
return problem;
}
}
Com sku="BOX-A", compara units=0, 3 e 6. Zero viola Min antes do método; três devolve um DTO; seis passa as restrições mas encontra a capacidade ilustrativa 4 e produz conflito. Nenhum ramo grava ou reserva stock.
Armadilhas comuns
Usar Valid como autorização; ignorar BindingResult; tratar apenas uma das exceções de validação; devolver 200 em todos os erros; copiar mensagens internas para o cliente; esperar que advice capture qualquer falha de filtros.
Tópicos relacionados: Rotas HTTP e contratos de pedidos · Segurança e fronteiras de autorização · Âmbito de testes e evidência de integração
A rejeição deve ocorrer na fronteira certa, com estado e corpo coerentes. Uma resposta estruturada informa o cliente, mas não corrige automaticamente o pedido nem garante que uma repetição seja segura.
Referência: MVC validation boundaries · Spring Boot 4.1.1 / Spring Framework 7.0.9 / Java 25