@RequestBody

Reads the request body, usually JSON, and converts it into a Java object such as a record DTO.

Signature
@Valid @RequestBody CreateThing body
  • Added in Spring Boot Spring Framework 3.0

Parameters

ParameterTypeDescription
$requiredoptionalboolean= trueWhether a body must be present. An empty body with required = true gives 400.

Return value

n/aConverts the request body into the parameter type using an HttpMessageConverter chosen by Content-Type (Jackson for application/json). Combine with @Valid to run Bean Validation.

Errors

HttpMessageNotReadableException (400) for missing or malformed bodies; MethodArgumentNotValidException (400) when @Valid fails; HttpMediaTypeNotSupportedException (415) when no converter reads the Content-Type.

@RequestBody tells Spring MVC to read the whole request body and turn it into the parameter's type. For JSON the work is done by Jackson through an HttpMessageConverter, chosen from the request's Content-Type. A request is only allowed one body, so a method has at most one @RequestBody parameter.

Use a request record

Bind the body to a record that contains exactly the fields a client may send. Binding to a JPA entity lets clients set fields they should never touch, such as id, owner or role (mass assignment), and ties your API to your table design. Records are immutable, Jackson binds them through their canonical constructor, and constraint annotations sit naturally on their components.

record PlaceOrder(@NotBlank String customerId,
                  @NotEmpty List<@Valid OrderLine> lines) {}

@PostMapping("/api/orders")
OrderPlaced place(@Valid @RequestBody PlaceOrder order)

What can go wrong, and the status you get

ProblemExceptionStatus
Body missing, malformed JSON, wrong type for a fieldHttpMessageNotReadableException400
@Valid constraints failMethodArgumentNotValidException400
Content-Type not supportedHttpMediaTypeNotSupportedException415

Spring handles all three for you. You only add handlers to shape the response body consistently, for example as problem details with field errors.

Mistakes people make

  • Forgetting @Valid. Constraints on the record are ignored until the parameter is annotated. Nested objects and list elements need @Valid too.
  • Echoing parser errors. The message from a HttpMessageNotReadableException can include Java type names. Return your own wording.
  • Using it for form posts. URL-encoded forms are parameters; read them with @RequestParam or @ModelAttribute.
  • Unbounded payloads. A JSON array of a million items is parsed into memory before validation runs. Limit body size at the proxy and cap list sizes with @Size.

Example

Example · java
import java.math.BigDecimal;
import java.util.List;

import jakarta.validation.Valid;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Positive;
import jakarta.validation.constraints.Size;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

record OrderLine(@NotBlank String sku, @Positive int quantity) {}

record PlaceOrder(@NotBlank String customerId,
                  @NotEmpty @Size(max = 100) List<@Valid OrderLine> lines,
                  @DecimalMin("0.00") BigDecimal tip) {}

record OrderPlaced(String orderId, int lineCount) {}

@RestController
public class CheckoutController {

    // JSON -> PlaceOrder via Jackson, then @Valid runs the constraints.
    // Malformed JSON: 400. Constraint failure: 400. Wrong Content-Type: 415.
    @PostMapping("/api/orders")
    @ResponseStatus(HttpStatus.CREATED)
    OrderPlaced place(@Valid @RequestBody PlaceOrder order) {
        return new OrderPlaced("ord_" + order.customerId(), order.lines().size());
    }
}

When to use it

  • Accepting a JSON order with nested line items and validating every line
  • Taking a partner's snake_case payload without renaming Java fields
  • Running a search endpoint whose filters are all optional

Frequently asked questions

Can I have two @RequestBody parameters?

No. The body stream can only be read once. Wrap both objects in one request record.

Why do I get 415 with a valid JSON body?

The client did not send Content-Type: application/json (or sent text/plain), so no converter claims the body. Fix the client's header.

More examples

Answering malformed JSON cleanly

Jackson failures surface as HttpMessageNotReadableException. The handler returns a problem detail with a fixed message instead of the parser text, which could reveal class names.

Example · java
import org.springframework.http.HttpStatus;
import org.springframework.http.ProblemDetail;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

record Signup(String email, int age) {}

@RestController
public class SignupController {

    @PostMapping("/api/signups")
    Signup signup(@RequestBody Signup body) {
        return body;
    }

    // {"email": "[email protected]", "age": "twelve"} or truncated JSON ends up here.
    // Do not echo the parser message: it can include internal type names.
    @ExceptionHandler(HttpMessageNotReadableException.class)
    ProblemDetail unreadable(HttpMessageNotReadableException ex) {
        ProblemDetail problem = ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST,
                "The request body is not valid JSON for this endpoint.");
        problem.setTitle("Malformed request body");
        return problem;
    }
}

Mapping snake_case fields

Jackson annotations still live in com.fasterxml.jackson.annotation with Jackson 3. @JsonProperty on a record component renames the JSON field for both reading and writing.

Example · java
import com.fasterxml.jackson.annotation.JsonProperty;

import java.time.LocalDate;

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

// The client sends snake_case; Java keeps its own naming.
record DeliveryRequest(@JsonProperty("postal_code") String postalCode,
                       @JsonProperty("delivery_date") LocalDate deliveryDate) {}

@RestController
public class DeliveryController {

    @PostMapping("/api/deliveries/quote")
    DeliveryRequest quote(@RequestBody DeliveryRequest request) {
        return request;
    }
}

An optional body

required = false lets the client send no body at all, in which case the parameter is null. It is handy for search endpoints where every filter has a default.

Example · java
import java.util.Map;

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

record SearchFilters(String text, Integer limit) {}

@RestController
public class SearchController {

    // An empty POST is allowed: filters is null and the search uses defaults.
    @PostMapping("/api/search")
    Map<String, Object> search(@RequestBody(required = false) SearchFilters filters) {
        String text = filters != null && filters.text() != null ? filters.text() : "";
        int limit = filters != null && filters.limit() != null ? filters.limit() : 20;
        return Map.of("text", text, "limit", limit);
    }
}

Discussion

  • Be the first to comment on this lesson.