Validating Request Bodies
The most common place to validate is the request body of a POST or PUT endpoint. Annotate the @RequestBody parameter with @Valid, and Spring runs every constraint on the deserialized object before your handler executes. This page covers the body, nested objects, collections, and exactly what happens when validation fails.
@Valid on @RequestBody
Place @Valid directly before @RequestBody. If any constraint is violated, Spring throws MethodArgumentNotValidException and the method body is skipped.
import jakarta.validation.Valid;
import jakarta.validation.constraints.*;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
public record CreateOrderRequest(
@NotBlank String customerEmail,
@Min(1) int quantity) {}
@RestController
@RequestMapping("/api/orders")
public class OrderController {
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public OrderResponse create(@Valid @RequestBody CreateOrderRequest request) {
return service.create(request);
}
}
Note: Use
@Validhere, not Spring’s@Validated. Both trigger validation of a@RequestBody, but@Validis the standard Jakarta annotation and is what you want unless you specifically need validation groups.
Nested object validation
Validation does not automatically recurse into nested objects. To validate a field that is itself a constrained object, annotate that field with @Valid so the constraints cascade.
public record Address(
@NotBlank String street,
@NotBlank @Size(min = 2, max = 2) String countryCode) {}
public record CreateCustomerRequest(
@NotBlank String name,
@NotNull
@Valid // cascade into Address constraints
Address address) {}
Without the @Valid on address, only @NotNull is checked — street and countryCode would be ignored. The cascade works to any depth: each level that should be validated needs its own @Valid.
Validating collections
For a collection of objects, cascade by placing @Valid on the type argument; for a collection of scalars, place the constraint on the element type.
public record OrderLine(
@NotBlank String sku,
@Min(1) int quantity) {}
public record BulkOrderRequest(
@NotEmpty
@Valid // validate each OrderLine
List<OrderLine> lines,
@NotEmpty
List<@NotBlank String> tags) {} // validate each tag string
When an element fails, the resulting error’s field path includes the index, e.g. lines[2].quantity.
What happens on failure
A failed @RequestBody validation throws MethodArgumentNotValidException, which Spring maps to 400 Bad Request. The exception carries a BindingResult listing every FieldError.
Request:
curl -X POST http://localhost:8080/api/orders \
-H "Content-Type: application/json" \
-d '{"customerEmail":"","quantity":0}'
Default Spring Boot output:
{
"timestamp": "2026-06-13T10:15:30.123+00:00",
"status": 400,
"error": "Bad Request",
"path": "/api/orders"
}
That default body hides which fields failed. To return a useful, structured payload you catch the exception in a @RestControllerAdvice and read its field errors:
import org.springframework.web.bind.MethodArgumentNotValidException;
@RestControllerAdvice
public class ValidationExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Map<String, String> onValidation(MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getFieldErrors()
.forEach(err -> errors.put(err.getField(), err.getDefaultMessage()));
return errors;
}
}
Output with the advice in place:
{
"customerEmail": "must not be blank",
"quantity": "must be greater than or equal to 1"
}
For the complete treatment — including ConstraintViolationException, ProblemDetail, and a reusable error shape — see Handling Validation Errors.
@Valid vs malformed JSON
The two failure modes are distinct and produce different exceptions:
| Situation | Exception | Status |
|---|---|---|
| JSON parses but violates a constraint | MethodArgumentNotValidException | 400 |
| JSON is malformed / wrong types | HttpMessageNotReadableException | 400 |
Validation runs only after successful deserialization. If the JSON can’t be parsed into the DTO at all, Jackson fails first and the constraints never run.
Warning: A record with a primitive field like
int quantitycannot receivenull— a missing JSON value defaults it to0, which then fails@Min(1)rather than@NotNull. Use a boxedIntegerwith@NotNullwhen “absent” must be distinguished from “zero”.
Validating multiple body shapes
Each @Valid @RequestBody parameter is validated independently. You can apply different DTOs (and thus different rules) to create and update endpoints, or use validation groups to vary the rules on a single shared DTO.