Skip to content
Spring Boot sb web 3 min read

Request & Response Body

REST APIs exchange structured payloads — usually JSON. Spring MVC binds the request body to a Java object with @RequestBody and serializes return values with @ResponseBody, both powered by Jackson. This page shows how deserialization works, how to return DTOs and records, and the basics of content negotiation.

@RequestBody — JSON to object

@RequestBody tells Spring to read the HTTP request body and deserialize it into the parameter type using the configured HttpMessageConverter (Jackson for JSON).

public record CreateUserRequest(String name, String email, int age) {}

@RestController
@RequestMapping("/api/users")
public class UserController {

    @PostMapping
    public User create(@RequestBody CreateUserRequest body) {
        return service.create(body);
    }
}

Request:

curl -X POST http://localhost:8080/api/users \
  -H "Content-Type: application/json" \
  -d '{"name":"Grace Hopper","email":"[email protected]","age":45}'

Jackson matches JSON fields to record components (or bean properties) by name. Unknown JSON fields are ignored by default in Spring Boot. A malformed body throws HttpMessageNotReadableException, surfaced as 400 Bad Request.

Tip: Records make excellent request DTOs — immutable, concise, and Jackson deserializes them through the canonical constructor. See Records as DTOs.

@ResponseBody and @RestController

@ResponseBody tells Spring to serialize the return value into the response body instead of resolving a view name. @RestController is @Controller + @ResponseBody, so every handler in it returns data, not a view.

@RestController            // implies @ResponseBody on every method
@RequestMapping("/api/users")
public class UserController {

    @GetMapping("/{id}")
    public User one(@PathVariable Long id) {  // returned object -> JSON
        return service.findById(id);
    }
}

In a plain @Controller, annotate the method (or its return) with @ResponseBody to opt into serialization.

Returning DTOs and records

Expose DTOs, not JPA entities. A DTO controls exactly what goes on the wire, avoids lazy-loading serialization traps, and decouples your API from your schema.

public record UserResponse(Long id, String name, String email) {}

@GetMapping("/{id}")
public UserResponse one(@PathVariable Long id) {
    User user = service.findById(id);
    return new UserResponse(user.getId(), user.getName(), user.getEmail());
}

Output:

{ "id": 42, "name": "Grace Hopper", "email": "[email protected]" }

For the full reasoning and mapping strategies, see the DTO Pattern.

Customizing Jackson

Tune serialization with annotations on the DTO or global properties.

public record UserResponse(
        Long id,
        String name,
        @JsonProperty("email_address") String email,
        @JsonInclude(JsonInclude.Include.NON_NULL) String nickname) {}

Global defaults live in application.properties:

spring.jackson.property-naming-strategy=SNAKE_CASE
spring.jackson.default-property-inclusion=non_null
spring.jackson.serialization.write-dates-as-timestamps=false
spring.jackson.time-zone=UTC
AnnotationPurpose
@JsonProperty("x")Rename a field in JSON
@JsonIgnoreExclude a field
@JsonInclude(NON_NULL)Omit nulls
@JsonFormat(pattern = ...)Format dates/numbers

Content negotiation basics

Spring picks a representation based on the request’s Accept header and the handler’s produces setting. JSON is the default. With jackson-dataformat-xml on the classpath, the same endpoint can serve XML.

curl http://localhost:8080/api/users/42 -H "Accept: application/json"

If the client requests a type the server cannot produce, the response is 406 Not Acceptable. You can constrain or expand the offered types per handler:

@GetMapping(value = "/{id}",
        produces = {MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE})
public UserResponse one(@PathVariable Long id) { ... }

Note: Spring Boot 3.x disables suffix-based negotiation (/users/42.json) by default. Drive negotiation through the Accept header, which is the standards-compliant approach.

Reading raw or partial bodies

For unusual cases you can bind the body to a String, a byte[], a Map, or JsonNode:

@PostMapping("/webhook")
public void webhook(@RequestBody Map<String, Object> payload) {
    log.info("Received {} keys", payload.size());
}

Pitfalls

  • A request DTO that is a record must have its components match JSON field names (or use @JsonProperty); there are no setters to fall back on.
  • Returning a JPA entity directly can trigger LazyInitializationException during serialization — map to a DTO first.
  • @RequestBody requires a body; pairing it with GET is almost always a mistake.
Last updated June 13, 2026
Was this helpful?