Walk through how Spring resolves a @RequestBody parameter, including converter selection and failure modes.
answer
- RequestResponseBodyMethodProcessor + HttpMessageConverter list
- canRead(type, contentType) → first match reads InputStream
- No converter = 415; malformed = HttpMessageNotReadableException 400
- Customize ObjectMapper / custom converter, NOT @InitBinder
- required defaults true; @Valid runs after read
basics
~20 sA 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 sFor 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@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
Know Jackson turns the JSON body into the object and a bad body gives a 400.
Describe converter selection by Content-Type and the 415 vs 400 distinction.
Detail RequestResponseBodyMethodProcessor, canRead ordering, required=false, and that customization is on the ObjectMapper.
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