Spring Boot JSON: @RequestBody, @ResponseBody, Jackson, and REST APIs

Last updated:

Spring Boot makes JSON handling nearly effortless: drop in spring-boot-starter-web, annotate your controller with @RestController, and Jackson takes care of serialization and deserialization automatically. But production REST APIs require more — validation, custom naming strategies, error response shapes, field visibility control, and custom type handling. This guide covers the full picture from basic controller setup through advanced Jackson configuration.

@RequestBody and @ResponseBody

@RestController combines @Controller and @ResponseBody, making every method return value serialize directly to the HTTP response body as JSON. Use ResponseEntity<T> when you need full control over the status code and headers — for example, 201 Created on POST or 204 No Content on DELETE.

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

    @GetMapping("/{id}")
    public ResponseEntity<User> getUser(@PathVariable Long id) {
        User user = userService.findById(id)
            .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
        return ResponseEntity.ok(user);
    }

    @PostMapping
    public ResponseEntity<User> createUser(@Valid @RequestBody UserCreateDto dto) {
        User created = userService.create(dto);
        URI location = URI.create("/api/users/" + created.getId());
        return ResponseEntity.created(location).body(created);
    }

    @PutMapping("/{id}")
    public User updateUser(@PathVariable Long id, @RequestBody UserUpdateDto dto) {
        return userService.update(id, dto);  // 200 OK with updated object
    }

    @DeleteMapping("/{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void deleteUser(@PathVariable Long id) {
        userService.delete(id);  // 204 No Content, no body
    }
}

Jackson ObjectMapper Configuration

Spring Boot auto-configures a global ObjectMapper from spring.jackson.* properties. Set naming strategy, null inclusion, and date format in application.yml; or define a Jackson2ObjectMapperBuilderCustomizer bean for programmatic control without replacing the auto-configuration.

# application.yml — Jackson global defaults
spring:
  jackson:
    default-property-inclusion: non_null   # omit null fields
    property-naming-strategy: SNAKE_CASE   # camelCase → snake_case
    serialization:
      write-dates-as-timestamps: false     # ISO 8601 dates
    deserialization:
      fail-on-unknown-properties: false    # ignore extra JSON fields

// Or programmatically
@Configuration
public class JacksonConfig {
    @Bean
    public ObjectMapper objectMapper() {
        return JsonMapper.builder()
            .addModule(new JavaTimeModule())
            .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
            .serializationInclusion(JsonInclude.Include.NON_NULL)
            .build();
    }
}

Jackson Annotations on POJOs

Jackson annotations let you control how individual fields and classes are serialized without touching the global ObjectMapper. Use them for renaming, excluding, formatting, and aliasing JSON properties.

public class Product {
    @JsonProperty("product_id")          // rename field in JSON
    private Long id;

    @JsonIgnore                           // never serialize this field
    private String internalCode;

    @JsonFormat(pattern = "yyyy-MM-dd")  // custom date format
    private LocalDate releaseDate;

    @JsonInclude(JsonInclude.Include.NON_NULL)  // omit if null (field-level)
    private String description;

    @JsonAlias({"product_name", "title"})  // accept alternate JSON keys on decode
    private String name;

    // getters, setters, or use @JsonAutoDetect with fields
}

DTOs and Validation

Use DTOs (Data Transfer Objects) to capture the exact shape of incoming JSON independently of the domain model. Annotate DTO fields with Bean Validation constraints and add @Valid to the controller parameter to trigger validation automatically.

public record UserCreateDto(
    @NotBlank(message = "Name is required")
    String name,

    @Email(message = "Must be a valid email")
    String email,

    @Min(value = 18, message = "Must be at least 18")
    int age
) {}

// Controller receives and validates
@PostMapping
public User create(@Valid @RequestBody UserCreateDto dto) { ... }

// Global exception handler for 400 Bad Request
@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public Map<String, String> handleValidation(MethodArgumentNotValidException e) {
        return e.getBindingResult().getFieldErrors().stream()
            .collect(Collectors.toMap(
                FieldError::getField,
                FieldError::getDefaultMessage
            ));
    }
}

Custom Serializer/Deserializer

When Jackson's default behavior doesn't fit a type — such as a Money value object or a third-party class you can't annotate — write a custom JsonSerializer or JsonDeserializer and register it on the class or globally.

// Custom serializer for Money type
public class MoneySerializer extends JsonSerializer<Money> {
    @Override
    public void serialize(Money money, JsonGenerator gen, SerializerProvider provider)
            throws IOException {
        gen.writeStartObject();
        gen.writeNumberField("amount", money.getAmount().toPlainString());
        gen.writeStringField("currency", money.getCurrency().getCurrencyCode());
        gen.writeEndObject();
    }
}

// Register on class
@JsonSerialize(using = MoneySerializer.class)
public class Money { ... }

// Or register globally
@Bean
public Jackson2ObjectMapperBuilderCustomizer customizer() {
    return builder -> builder.serializerByType(Money.class, new MoneySerializer());
}

JSON Response Shaping

Use @JsonView to return different subsets of fields depending on the caller's role, without creating multiple DTOs. Spring Projections offer an interface-based alternative for Spring Data repositories.

// @JsonView — return different fields for public vs admin
public class Views {
    public static class Public {}
    public static class Admin extends Public {}
}

public class User {
    @JsonView(Views.Public.class) private String name;
    @JsonView(Views.Admin.class) private String email;   // admin only
    @JsonView(Views.Admin.class) private String role;    // admin only
}

@GetMapping("/{id}")
@JsonView(Views.Public.class)
public User getUser(@PathVariable Long id) { ... }

// Alternatively, use Spring Projections (interface-based)
public interface UserSummary {
    String getName();
    String getEmail();
}

JSON Error Responses

Spring 6 (Boot 3+) ships built-in support for RFC 9457 Problem Details via the ProblemDetail class. Return structured, machine-readable error bodies without building a custom error DTO from scratch.

// Spring 6+ built-in Problem Details (RFC 9457)
@RestControllerAdvice
public class ProblemDetailsHandler extends ResponseEntityExceptionHandler {

    @ExceptionHandler(ResourceNotFoundException.class)
    public ProblemDetail handleNotFound(ResourceNotFoundException ex) {
        ProblemDetail problem = ProblemDetail.forStatusAndDetail(
            HttpStatus.NOT_FOUND, ex.getMessage()
        );
        problem.setTitle("Resource Not Found");
        problem.setType(URI.create("https://api.example.com/errors/not-found"));
        return problem;
    }
}

// Response JSON:
// {"type":"https://api.example.com/errors/not-found",
//  "title":"Resource Not Found","status":404,
//  "detail":"User 42 not found"}

Key Jackson Annotations Reference

AnnotationApplies toEffect
@JsonProperty("name")Field / getterOverride JSON key name
@JsonIgnoreField / getterExclude from serialization and deserialization
@JsonInclude(NON_NULL)Class / fieldOmit null values from output
@JsonFormat(pattern=)Date / time fieldCustom date/time format string
@JsonAlias(...)FieldAccept alternative keys during deserialization
@JsonView(Class)Field / methodInclude field only for the specified view
@JsonNaming(Strategy)ClassApply naming strategy (e.g. SNAKE_CASE) to all fields

Definitions

ObjectMapper
The central Jackson class that converts Java objects to JSON strings (serialization) and JSON strings to Java objects (deserialization).
@RestController
A Spring MVC stereotype annotation combining @Controller and @ResponseBody; every method return value is serialized to JSON (or XML) response body.
@RequestBody
Marks a method parameter for binding the incoming HTTP request body, deserialized from JSON by Jackson.
HttpMessageConverter
Spring's interface for reading HTTP requests and writing responses; MappingJackson2HttpMessageConverter handles application/json.
DTO (Data Transfer Object)
A plain Java class used to capture request/response shape independently of the domain model; prevents over-posting (mass assignment) and exposes only intended fields.

FAQ

How does Spring Boot automatically serialize objects to JSON?

Spring Boot auto-configures Jackson via spring-boot-autoconfigure when jackson-databind is on the classpath — included through spring-boot-starter-web. When a controller method is annotated with @RestController (or @ResponseBody), the return value is passed to the HttpMessageConverter chain. MappingJackson2HttpMessageConverter intercepts calls with Content-Type: application/json, invokes the auto-configured ObjectMapper, and writes the serialized JSON to the response body. The ObjectMapper uses BeanSerializer to introspect getters (or fields with @JsonAutoDetect) and converts each property to a JSON key-value pair. You can inspect the registered converters via WebMvcConfigurer or by printing the WebApplicationContext bean list.

What is the difference between @RequestBody and @ModelAttribute in Spring?

@RequestBody binds the raw HTTP request body to a method parameter by deserializing its content — typically JSON or XML — using an HttpMessageConverter. Use it for REST APIs where the client sends application/json. @ModelAttribute binds form data (application/x-www-form-urlencoded or multipart/form-data) or query-string parameters to a Java object using Spring's data binding mechanism; it does not read the request body. You cannot apply both annotations to the same parameter. A common mistake is using @RequestBody for HTML form submissions — Spring will fail to parse the body because the Content-Type is form-encoded, not JSON.

How do I ignore null fields in Spring Boot JSON responses?

The simplest approach is spring.jackson.default-property-inclusion=non_null in application.yml. This configures the global ObjectMapper to omit any field whose value is Java null. Alternatively, annotate a specific class with @JsonInclude(JsonInclude.Include.NON_NULL) to apply the rule only to that type, or annotate a single field for field-level control. NON_NULL skips null properties. NON_EMPTY also skips empty strings, empty collections, and empty maps. NON_ABSENT (Jackson 2.6+) additionally skips Optional.empty() values. For the most control, define an ObjectMapper @Bean — but note this replaces Spring Boot's auto-configuration entirely.

How do I handle snake_case JSON in Spring Boot with camelCase Java fields?

Set spring.jackson.property-naming-strategy=SNAKE_CASE in application.yml to apply snake_case globally. Jackson will automatically convert camelCase Java field names to snake_case JSON keys on serialization and reverse the mapping on deserialization. Field mapping examples: firstNamefirst_name, createdAtcreated_at, isActiveis_active. Alternatively, annotate a specific class with @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) to limit the strategy to that class. For individual fields, use @JsonProperty("first_name") to set the JSON key explicitly — the best option when dealing with acronyms like productSKU.

How do I validate JSON request body in Spring Boot?

Add @Valid (JSR-380) or @Validated (Spring) before @RequestBody in the controller method signature. Annotate DTO fields with constraints: @NotNull, @NotBlank, @Email, @Min, @Max, @Size, @Pattern. When validation fails, Spring throws MethodArgumentNotValidException. Handle it in a @RestControllerAdvice to return a 400 Bad Request with a structured error body. The difference between @Valid and @Validated: @Valid is standard JSR-380 and supports cascaded validation via @Valid on nested fields. @Validated is Spring-specific and additionally supports validation groups, letting you apply different constraints depending on the operation (create vs. update).

How do I customize the Jackson ObjectMapper in Spring Boot?

Three approaches in increasing order of invasiveness: (1) application.yml spring.jackson.* properties — the simplest option; covers naming strategy, null inclusion, date format, and feature flags with no Java code. (2) Jackson2ObjectMapperBuilderCustomizer @Bean — extends Spring Boot's default builder without replacing it; add modules, serializers, and features while preserving auto-configured defaults like JavaTimeModule registration. (3) ObjectMapper @Bean — full control but replaces auto-configuration entirely; you must manually register all modules. The customizer approach is recommended for most production use cases because it preserves Spring Boot's sensible defaults while allowing targeted changes.

How do I return a 201 Created with JSON body in Spring Boot?

Return ResponseEntity.created(location).body(dto) from a @PostMapping method, where location is a java.net.URI pointing to the newly created resource. The created() factory method sets the HTTP status to 201 and adds a Location header automatically. Build the URI with URI.create("/api/resources/" + created.getId()) or use UriComponentsBuilder.fromCurrentRequest().path("/{id}").buildAndExpand(created.getId()).toUri() for an absolute URI based on the incoming request. For void methods returning 204 No Content, use @ResponseStatus(HttpStatus.NO_CONTENT) or return ResponseEntity.noContent().build().

What Jackson module do I need for Java 8 dates in Spring Boot?

You need jackson-datatype-jsr310, which provides JavaTimeModule — adding serializers and deserializers for LocalDate, LocalDateTime, ZonedDateTime, Instant, Duration, and other java.time types. Spring Boot auto-registers JavaTimeModule if jackson-datatype-jsr310 is on the classpath — it is included transitively through spring-boot-starter-web, so no explicit dependency is typically needed. However, auto-registration still serializes dates as numeric arrays by default. Set spring.jackson.serialization.write-dates-as-timestamps=false in application.yml to get ISO 8601 strings instead (e.g., "2026-05-19T10:30:00Z"). For a custom format on a specific field, use @JsonFormat(pattern = "yyyy-MM-dd", timezone = "UTC").

Further reading and primary sources

  • Spring Web MVC JSON docsOfficial Spring reference for @RequestBody, @ResponseBody, and HttpMessageConverter configuration
  • Jackson ObjectMapper docsFull Javadoc for ObjectMapper including serialization, deserialization, and module registration
  • RFC 9457 Problem DetailsThe RFC specification for Problem Details for HTTP APIs, implemented by Spring 6 ProblemDetail