skip to content

Walk through how Spring resolves a @RequestBody parameter, including converter selection and failure modes.

level: principalimportance: should knowfreq 38%

answer

  1. RequestResponseBodyMethodProcessor + HttpMessageConverter list
  2. canRead(type, contentType) → first match reads InputStream
  3. No converter = 415; malformed = HttpMessageNotReadableException 400
  4. Customize ObjectMapper / custom converter, NOT @InitBinder
  5. required defaults true; @Valid runs after read

basics

~20 s

A HandlerMethodArgumentResolver reads the request's InputStream, picks an HttpMessageConverter that supports the target type and the request's Content-Type, and uses it (e.g. Jackson) to deserialize the body. No match yields 415; a malformed body yields HttpMessageNotReadableException (400).

solid answer

~40 s

For a @RequestBody parameter, RequestResponseBodyMethodProcessor (a HandlerMethodArgumentResolver / HttpMessageConverter-backed resolver) handles it. It inspects the request's Content-Type, then iterates the ordered list of registered HttpMessageConverters asking each canRead(targetType, mediaType); the first match reads the body from the ServletInputStream. For application/json that's typically MappingJackson2HttpMessageConverter delegating to an ObjectMapper, honoring Jackson annotations and features. Failure modes: no converter supports the Content-Type → HttpMediaTypeNotSupportedException (415); body present but unparseable/type-mismatch → HttpMessageNotReadableException (400); missing body when required=true → also fails (required defaults true). This path bypasses WebDataBinder entirely, so @InitBinder, PropertyEditors and allowed-field lists don't apply — customization happens on the ObjectMapper (modules, features, mixins) or via a custom converter. @Valid on @RequestBody runs after successful read, surfacing MethodArgumentNotValidException.

code

java · 21 lines
java
@Configuration
class WebConfig implements WebMvcConfigurer {
    // Register/extend converters that back @RequestBody reading
    @Override
    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        ObjectMapper mapper = JsonMapper.builder()
            .addModule(new JavaTimeModule())
            .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true)
            .build();
        converters.add(0, new MappingJackson2HttpMessageConverter(mapper));
    }
}

@RestController
class OrderController {
    // required=false -> null body is allowed instead of 400
    @PostMapping("/orders")
    Order create(@RequestBody(required = false) OrderDto dto) {
        return dto == null ? Order.empty() : service.create(dto);
    }
}

go deeper

for a junior

Know Jackson turns the JSON body into the object and a bad body gives a 400.

for a middle

Describe converter selection by Content-Type and the 415 vs 400 distinction.

for a senior

Detail RequestResponseBodyMethodProcessor, canRead ordering, required=false, and that customization is on the ObjectMapper.

for a principal

Design the converter/ObjectMapper strategy, reason about media-type negotiation and error contracts, and keep binding-vs-body customization cleanly separated.

## End-to-end resolution of @RequestBody ### 1. Argument resolver selection Every controller-method parameter is resolved by a `HandlerMethodArgumentResolver`. For `@RequestBody`, that is `RequestResponseBodyMethodProcessor` (which also handles `@ResponseBody` on the return side). `RequestMappingHandlerAdapter` holds the ordered resolver list and the ordered `HttpMessageConverter` list. ### 2. Content negotiation for reading The processor extends `AbstractMessageConverterMethodArgumentResolver`. It: 1. Reads the request `Content-Type` (defaulting to `application/octet-stream` if absent). 2. Iterates the **registered converters in order**, calling `converter.canRead(targetType, contentType)`. 3. The **first** converter that returns true wins and its `read(...)` deserializes the body from the request `InputStream`. Converter examples: `MappingJackson2HttpMessageConverter` (JSON), `MappingJackson2XmlHttpMessageConverter`/`Jaxb2RootElementHttpMessageConverter` (XML), `StringHttpMessageConverter` (text), `ByteArrayHttpMessageConverter`. Order matters — the first capable one is used, so a custom converter registered earlier can override defaults. ### 3. Jackson specifics (the common case) `MappingJackson2HttpMessageConverter` wraps a Jackson `ObjectMapper`. Behavior is driven by: - Jackson annotations: `@JsonProperty`, `@JsonIgnore`, `@JsonCreator`, `@JsonProperty(access = READ_ONLY)`, `@JsonFormat`. - `ObjectMapper` features: e.g. `DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES` (Boot default **on**), `FAIL_ON_NULL_FOR_PRIMITIVES`. - Registered modules: `JavaTimeModule` (java.time), `Kotlin` module, etc. Customize via a `Jackson2ObjectMapperBuilderCustomizer` / `@JsonComponent` / properties — **not** via `@InitBinder`. ### 4. Failure modes and status codes - **No converter for the Content-Type** → `HttpMediaTypeNotSupportedException` → HTTP **415 Unsupported Media Type**. - **Body unreadable / JSON malformed / type mismatch** (e.g. `"abc"` into an `int`) → `HttpMessageNotReadableException` → HTTP **400 Bad Request**. Root cause is often a Jackson `JsonParseException`/`MismatchedInputException`. - **Missing/empty body when `@RequestBody(required = true)`** (the default) → read failure → 400. Set `required = false` (or use `Optional`) to allow a null body. - **Unknown property with FAIL_ON_UNKNOWN_PROPERTIES on** → `HttpMessageNotReadableException` (400). These exceptions are typically translated by `ResponseEntityExceptionHandler` / `DefaultHandlerExceptionResolver` (or Boot's error handling). The mapping between exception and status is Exception-Handling-leaf territory, but knowing which exception is thrown is part of understanding the read path. ### 5. What does NOT happen - `WebDataBinder` is **not** invoked. `@InitBinder`, `registerCustomEditor`, `setAllowedFields`, and app `Converter`/`Formatter`s registered for web binding do **not** affect `@RequestBody`. Over-posting defense here is Jackson-side (DTOs, `@JsonIgnore`, read-only access). - Request parameters (query string, form fields) are **not** merged into the body object. ### 6. Validation timing If the parameter is `@Valid`/`@Validated`, validation runs **after** a successful read; failures raise `MethodArgumentNotValidException` (400). If the body can't even be read, you never reach validation. ### When to customize - Need a non-JSON format or a bespoke media type → register a custom `HttpMessageConverter` (via `WebMvcConfigurer.configureMessageConverters`/`extendMessageConverters`). - Need different JSON rules → configure the `ObjectMapper`. - Need to accept a nullable/absent body → `@RequestBody(required = false)` or `Optional<T>`.

  • How does Spring choose between multiple registered HttpMessageConverters for one request?
    It iterates the ordered converter list and calls canRead(targetType, contentType) on each; the first that returns true is used. Order therefore matters — inserting a custom converter at the front lets it override defaults for that type/media type.
  • A client sends valid JSON but with an extra field the DTO doesn't have, and you get a 400. Why, and how would you allow it?
    Boot enables DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, so Jackson throws MismatchedInputException, surfaced as HttpMessageNotReadableException (400). Disable that feature on the ObjectMapper (or add @JsonIgnoreProperties(ignoreUnknown=true)) to accept and drop unknown fields.

saying these in an interview costs you the question

  • Saying WebDataBinder or @InitBinder deserializes @RequestBody
  • Claiming a wrong Content-Type gives 400 (it's 415)
  • Thinking @RequestBody is optional by default (required defaults true)
  • Believing query parameters get merged into the @RequestBody object

context