Spring @RequestBody and @ResponseBody: Request-Response Body Binding
Master Spring's @RequestBody and @ResponseBody annotations for HTTP message conversion.
20+ years shipping production Java in banking & fintech. Notes here come from systems that actually shipped.
- ✓Java 17+
- ✓Spring Boot 3.x
- ✓Basic understanding of REST APIs
- ✓Familiarity with JSON
- @RequestBody binds HTTP request body to a Java object using an HttpMessageConverter (e.g., Jackson for JSON).
- @ResponseBody tells Spring to write the return value directly to the HTTP response body, skipping view resolution.
- Both annotations work with Spring MVC's message conversion system, supporting JSON, XML, and other formats.
- Use @RestController to avoid adding @ResponseBody to every method.
- Always validate @RequestBody with @Valid and handle errors with a global exception handler.
Think of @RequestBody as a translator that converts incoming JSON (like a text message) into a Java object your code can understand. @ResponseBody does the opposite: it takes your Java object and converts it back to JSON to send back to the client. Without them, you'd have to manually parse and write JSON, which is error-prone and tedious.
If you've built any REST API with Spring Boot, you've probably used @RequestBody and @ResponseBody. They seem simple: @RequestBody deserializes the HTTP request body into a Java object, and @ResponseBody serializes the return value into the response body. But under the hood, there's a sophisticated message conversion pipeline that can surprise you when things go wrong.
I've lost count of how many times I've seen developers scratch their heads over a 415 Unsupported Media Type or a 400 Bad Request with no clear error message. Or worse, silent failures where the request body is just null. These aren't bugs in Spring—they're misunderstandings about how content negotiation and message converters work.
In this article, we'll go beyond the basics. You'll learn exactly how Spring selects a message converter, how to customize it, and what happens when the conversion fails. We'll cover real production scenarios, from a fintech startup's 2 AM outage caused by a missing getter to a SaaS billing system that silently swallowed validation errors. By the end, you'll have a battle-tested mental model for request-response body binding.
How @RequestBody Works Under the Hood
When Spring MVC receives a request with a body, the DispatcherServlet hands it off to the appropriate handler method. If the parameter is annotated with @RequestBody, Spring's RequestResponseBodyMethodProcessor kicks in. This processor uses the HttpMessageConverter interface to deserialize the request body into the target type.
The key player is the Content-Type header. Spring's default converters include MappingJackson2HttpMessageConverter for application/json, MappingJackson2XmlHttpMessageConverter for application/xml (if Jackson XML extension is on the classpath), and StringHttpMessageConverter for text/plain. The converter is selected based on the request's Content-Type and the target type.
Here's a simplified flow: 1. Spring checks the Content-Type header of the request. 2. It iterates over registered HttpMessageConverters, calling canRead() on each. 3. The first converter that returns true reads the body and returns the deserialized object. 4. If no converter can read, Spring throws HttpMediaTypeNotSupportedException (HTTP 415).
This is why sending XML to an endpoint expecting JSON fails with 415 unless you have the XML converter on the classpath.
One common pitfall: if your DTO doesn't have a no-args constructor, Jackson will fail to deserialize. You can use @JsonCreator with @JsonProperty on constructor parameters as an alternative. Also note that records (Java 16+) are naturally immutable and work well with Jackson because they implicitly have a canonical constructor.
equals(), and hashCode() based on all fields, which can cause lazy-loading issues or circular references. Use @Getter and @Setter specifically for DTOs.How @ResponseBody Works Under the Hood
@ResponseBody is the mirror image of @RequestBody. Instead of deserializing the request body, it serializes the return value of the handler method into the response body. The same HttpMessageConverter mechanism is used, but now it's driven by the Accept header of the request (or the produces attribute of the mapping).
When you annotate a method with @ResponseBody, Spring's RequestResponseBodyMethodProcessor writes the return value to the response using a converter that can write the object to the requested content type. If the client sends Accept: application/json, Spring will use MappingJackson2HttpMessageConverter. If Accept: application/xml, it will use the XML converter (if available).
The default behavior is to use the first converter that can write the object to the requested content type. If no converter matches, Spring throws HttpMediaTypeNotAcceptableException (HTTP 406).
A common mistake is forgetting @ResponseBody and returning a view name. This results in a 404 or a blank page because Spring tries to find a view with that name. The fix is simple: use @RestController instead of @Controller, which implicitly adds @ResponseBody to all methods.
Another gotcha: when returning ResponseEntity<?>, the @ResponseBody annotation on the method is still needed (unless using @RestController). The body of the ResponseEntity is what gets serialized. If you return ResponseEntity.ok().build() without a body, the response will have no body.
Customizing Message Converters
Spring Boot auto-configures a set of HttpMessageConverters based on what's on the classpath. By default, you get Jackson for JSON, and if you have Jackson XML or JAXB, you get XML support. But sometimes you need to customize the converters—for example, to change date formats, enable pretty printing, or add support for a custom media type.
To customize Jackson globally, you can configure ObjectMapper as a bean. Spring Boot will automatically use it for all JSON serialization/deserialization.
If you need to add a custom converter (e.g., for a proprietary binary format), you can implement HttpMessageConverter and register it via WebMvcConfigurer's configureMessageConverters() method. Be careful: if you override configureMessageConverters(), you lose the default converters. Use extendMessageConverters() instead to add your own without removing the defaults.
A common customization is to disable Jackson's FAIL_ON_EMPTY_BEANS to allow serialization of objects without getters (though this is a code smell). Another is to enable INDENT_OUTPUT for pretty-printed JSON in development.
One production gotcha: if you have multiple converters that can handle the same media type, the order matters. Spring uses the first converter that can read/write. If you add a custom converter, it will be added to the end of the list by default. Use the order property or implement Ordered to control precedence.
What the Official Docs Won't Tell You
The official Spring documentation covers the basics well, but there are several production realities they don't emphasize enough.
1. Silent failures on missing properties. By default, Jackson ignores unknown properties during deserialization. This means if your API adds a new field to the request body, but the DTO doesn't have it, Jackson silently ignores it. This can lead to subtle bugs where data is lost without any error. Always configure FAIL_ON_UNKNOWN_PROPERTIES = true in development, and consider using a DTO versioning strategy.
2. The no-args constructor trap. Jackson needs a no-args constructor (or a constructor annotated with @JsonCreator) to deserialize objects. If your DTO has only a parameterized constructor, Jackson will throw an exception. Lombok's @NoArgsConstructor can help, but be aware that it breaks immutability. Records solve this elegantly.
3. Content negotiation can be unpredictable. If your endpoint doesn't specify produces, Spring will use the Accept header to determine the response format. This can lead to XML responses when the client expects JSON if the Accept header is set to / and the XML converter is on the classpath. Always specify produces explicitly for critical endpoints.
4. @RequestBody is not for file uploads. For multipart file uploads, use @RequestParam with MultipartFile. @RequestBody will try to deserialize the entire body as a single object, which won't work for multipart data.
5. Circular references in serialization. JPA entities with bidirectional relationships will cause infinite recursion during serialization. Use @JsonIgnoreProperties or DTOs to break the cycle. Never expose JPA entities directly in REST APIs.
Validation and Error Handling
A REST API is only as good as its error messages. Without proper validation, a malformed request results in a cryptic 400 Bad Request with no details. Spring provides a clean way to handle this: combine @Valid with a @RestControllerAdvice.
When you annotate @RequestBody with @Valid, Spring's MethodArgumentNotValidException is thrown if validation fails. You can catch this in a global exception handler and return a structured error response.
For deserialization errors (e.g., invalid JSON, type mismatch), Spring throws HttpMessageNotReadableException. You should handle that too.
- Use a consistent error response format with a code, message, and details (list of field errors).
- Log the full stack trace on the server but return a sanitized message to the client.
- Never expose internal details like stack traces or SQL errors.
- For validation errors, include the field name, rejected value, and reason.
One advanced technique: use @JsonCreator with @JsonProperty to control which constructor parameters are used, and add validation annotations on the constructor parameters. This ensures that even if someone creates the object via the constructor (e.g., in tests), validation is enforced.
Performance Considerations
Serialization and deserialization can be a bottleneck in high-throughput APIs. Jackson is fast, but there are ways to make it faster.
Use Jackson's afterburner module. This module generates bytecode to optimize getter/setter calls, improving serialization performance by 20-30%. Add it as a dependency and Jackson will automatically use it.
Avoid unnecessary wrapping. If your response is a simple object, return it directly instead of wrapping it in a ResponseEntity. Spring will still handle status codes via @ResponseStatus.
Use streams for large payloads. If you're dealing with very large JSON arrays, consider using Jackson's JsonParser directly with a streaming approach instead of deserializing the entire array into a list. This reduces memory usage.
Enable Jackson's afterburner. Add the dependency: com.fasterxml.jackson.module:jackson-module-afterburner.
Caching serialized responses. For read-heavy endpoints, consider caching the serialized JSON string in a cache like Redis. This avoids repeating serialization for the same data.
Use primitive types where possible. Jackson handles primitives faster than wrapper types. Avoid using Boolean when boolean suffices.
One gotcha: afterburner module may not work with some advanced features like @JsonView or custom serializers. Test thoroughly.
The Missing Getter That Crashed Billing at 2 AM
- Always use Jackson's DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES during development.
- Write integration tests that verify @RequestBody deserialization with actual JSON payloads.
- Enable FAIL_ON_MISSING_CREATOR_PROPERTIES to catch missing properties early.
- Use @JsonIgnoreProperties(ignoreUnknown = false) to fail on unknown fields in production-critical endpoints.
- Consider using records (Java 16+) for DTOs to avoid getter/setter issues.
curl -v -H 'Content-Type: application/json' -d '{"key":"value"}' http://localhost:8080/api/endpointCheck log for 'HttpMessageNotWritableException' or 'HttpMediaTypeNotSupportedException'| File | Command / Code | Purpose |
|---|---|---|
| RequestBodyExample.java | @PostMapping("/api/invoices") | How @RequestBody Works Under the Hood |
| ResponseBodyExample.java | @RestController | How @ResponseBody Works Under the Hood |
| JacksonConfig.java | @Configuration | Customizing Message Converters |
| FailingDeserialization.java | public class InvoiceRequest { | What the Official Docs Won't Tell You |
| GlobalExceptionHandler.java | @RestControllerAdvice | Validation and Error Handling |
| PerformanceConfig.java | @Configuration | Performance Considerations |
Key takeaways
Interview Questions on This Topic
Explain how @RequestBody works in Spring MVC. What happens if the Content-Type is application/xml?
Frequently Asked Questions
20+ years shipping production Java in banking & fintech. Notes here come from systems that actually shipped.
That's Spring Boot. Mark it forged?
5 min read · try the examples if you haven't