Skip to content
Spring Boot sb validation 3 min read

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 @Valid here, not Spring’s @Validated. Both trigger validation of a @RequestBody, but @Valid is 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:

SituationExceptionStatus
JSON parses but violates a constraintMethodArgumentNotValidException400
JSON is malformed / wrong typesHttpMessageNotReadableException400

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 quantity cannot receive null — a missing JSON value defaults it to 0, which then fails @Min(1) rather than @NotNull. Use a boxed Integer with @NotNull when “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.

Last updated June 13, 2026
Was this helpful?