JSON in Spring Boot: Jackson, @RequestBody, @ResponseBody & Custom Serialization
Last updated:
Spring Boot auto-configures Jackson's ObjectMapper so every @RestController method serializes its return value to JSON and deserializes @RequestBody parameters from JSON automatically — no manual ObjectMapper setup required. @GetMapping returning a Java object produces Content-Type: application/json with the object's fields as JSON keys, using camelCase by default. @PostMapping with @RequestBody UserRequest body parses the incoming JSON into a typed Java object and throws HttpMessageNotReadableException (HTTP 400) if the body is missing or malformed. Jackson maps Java field names directly — add @JsonProperty("user_name") to override a specific field name, or configure spring.jackson.property-naming-strategy=SNAKE_CASE globally. This guide covers Jackson's auto-configuration, @RequestBody/@ResponseBody lifecycle, @JsonProperty, @JsonIgnore, @JsonIgnoreProperties, global ObjectMapper customization via application.properties, custom JsonSerializer/JsonDeserializer implementations, and handling polymorphic JSON with @JsonTypeInfo.
How Spring Boot Auto-Configures Jackson ObjectMapper
Adding spring-boot-starter-web to your build pulls in jackson-databind and registers a JacksonAutoConfiguration class that creates the primary ObjectMapper bean. Spring Boot applies sensible defaults: camelCase field naming, ISO 8601 dates when the JSR-310 module is present, and Content-Type: application/json for all @RestController responses. The MappingJackson2HttpMessageConverter picks up this ObjectMapper and handles all JSON serialization and deserialization transparently.
// ── pom.xml ────────────────────────────────────────────────────
// <dependency>
// <groupId>org.springframework.boot</groupId>
// <artifactId>spring-boot-starter-web</artifactId>
// </dependency>
// Pulls in: jackson-databind, jackson-core, jackson-annotations,
// jackson-datatype-jsr310 (Java 8 date/time support)
// ── Minimal @RestController — zero ObjectMapper config needed ──
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/users")
public class UserController {
// GET /api/users/1 → {"id":1,"name":"Alice","email":"alice@example.com"}
@GetMapping("/{id}")
public UserResponse getUser(@PathVariable Long id) {
return new UserResponse(id, "Alice", "alice@example.com");
}
// Jackson serializes the record to JSON automatically
record UserResponse(Long id, String name, String email) {}
}
// ── Auto-configuration hook points ────────────────────────────
// 1. application.properties: spring.jackson.* keys
// 2. Jackson2ObjectMapperBuilderCustomizer bean
// 3. Full ObjectMapper bean override (replaces auto-config)
// ── Customizer approach (recommended) ─────────────────────────
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.SerializationFeature;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class JacksonConfig {
@Bean
public Jackson2ObjectMapperBuilderCustomizer customizer() {
return builder -> builder
// Serialize dates as ISO 8601 strings, not numeric timestamps
.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
// Don't fail if JSON contains fields not in the POJO
.featuresToDisable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
// Omit null fields from all responses
.serializationInclusion(com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL);
}
}
// ── application.properties equivalents ────────────────────────
// spring.jackson.serialization.write-dates-as-timestamps=false
// spring.jackson.deserialization.fail-on-unknown-properties=false
// spring.jackson.default-property-inclusion=non_nullSpring Boot registers over 30 default Jackson configurations via JacksonProperties. The auto-configured ObjectMapper is available for injection with @Autowired ObjectMapper objectMapper if you need to call writeValueAsString() or readValue() directly. Declaring your own @Bean ObjectMapper replaces the auto-configuration entirely, so prefer the Jackson2ObjectMapperBuilderCustomizer pattern for incremental changes that preserve Spring Boot's defaults.
@RequestBody and @ResponseBody: JSON Request/Response Lifecycle
@RequestBody binds the HTTP request body to a Java method parameter by invoking MappingJackson2HttpMessageConverter.read(), which calls ObjectMapper.readValue(inputStream, targetType). If the body is missing, Jackson throws HttpMessageNotReadableException (HTTP 400). @ResponseBody (implicit on @RestController) serializes the return value by calling ObjectMapper.writeValueAsString(returnValue) and writing the result with Content-Type: application/json.
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import jakarta.validation.Valid;
@RestController
@RequestMapping("/api/orders")
public class OrderController {
// ── @RequestBody — deserializes JSON → Java object ────────
// POST /api/orders
// Body: {"productId":"abc","quantity":3,"price":29.99}
@PostMapping
public ResponseEntity<OrderResponse> createOrder(
@Valid @RequestBody CreateOrderRequest request) {
// request.productId(), request.quantity(), request.price()
// are fully typed after Jackson deserialization
Long orderId = orderService.create(request);
return ResponseEntity
.status(HttpStatus.CREATED) // 201
.body(new OrderResponse(orderId, "PENDING"));
}
// ── @ResponseBody (implicit) — serializes Java → JSON ─────
// GET /api/orders/42 → {"orderId":42,"status":"SHIPPED"}
@GetMapping("/{id}")
public OrderResponse getOrder(@PathVariable Long id) {
return orderService.findById(id); // Jackson serializes
}
// ── ResponseEntity for custom status + headers ─────────────
@PutMapping("/{id}/cancel")
public ResponseEntity<Void> cancelOrder(@PathVariable Long id) {
orderService.cancel(id);
return ResponseEntity.noContent().build(); // 204, no body
}
// ── Request / Response DTOs ────────────────────────────────
record CreateOrderRequest(
String productId,
int quantity,
double price
) {}
record OrderResponse(Long orderId, String status) {}
}
// ── Error cases for @RequestBody ──────────────────────────────
// 1. Missing body or empty body → HttpMessageNotReadableException → 400
// 2. Malformed JSON (syntax error) → HttpMessageNotReadableException → 400
// 3. Type mismatch (string where int expected) → HttpMessageConversionException → 400
// 4. @Valid constraint violation → MethodArgumentNotValidException → 400/422
// ── Global exception handler for request body errors ──────────
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.http.converter.HttpMessageNotReadableException;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(HttpMessageNotReadableException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResponse handleUnreadable(HttpMessageNotReadableException ex) {
return new ErrorResponse("INVALID_JSON", ex.getMessage());
}
record ErrorResponse(String code, String message) {}
}The full lifecycle: HTTP request arrives → DispatcherServlet routes to the controller method → Spring resolves @RequestBody by selecting MappingJackson2HttpMessageConverter (chosen because Content-Type: application/json matches) → Jackson deserializes the stream into the parameter type → controller method executes → return value is passed to MappingJackson2HttpMessageConverter.write() → Jackson serializes to a JSON string → written to the response body. The entire process is synchronous and handled within the Servlet thread unless you use reactive WebFlux.
Controlling Field Names with @JsonProperty and Naming Strategies
Jackson uses Java field names as JSON keys by default, preserving camelCase. @JsonProperty("snake_case_name") overrides the key for a specific field — the Java API stays unchanged while the JSON representation uses the annotated name. For global renaming, configure a PropertyNamingStrategies value: SNAKE_CASE converts firstName to first_name, UPPER_CAMEL_CASE produces FirstName, and LOWER_CASE produces firstname.
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
// ── Per-field @JsonProperty ────────────────────────────────────
// Java: UserProfile.userId → JSON: "user_id"
// Java: UserProfile.firstName → JSON: "first_name"
public class UserProfile {
@JsonProperty("user_id")
private Long userId;
@JsonProperty("first_name")
private String firstName;
@JsonProperty("last_name")
private String lastName;
// Standard getters/setters (or use Lombok @Data)
public Long getUserId() { return userId; }
public void setUserId(Long userId) { this.userId = userId; }
public String getFirstName() { return firstName; }
public void setFirstName(String firstName) { this.firstName = firstName; }
public String getLastName() { return lastName; }
public void setLastName(String lastName) { this.lastName = lastName; }
}
// Serialized: {"user_id":1,"first_name":"Alice","last_name":"Smith"}
// ── @JsonNaming on the class (class-level strategy) ────────────
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public class ProductDTO {
private String productName; // → "product_name"
private Double basePrice; // → "base_price"
private Integer stockCount; // → "stock_count"
// getters/setters omitted for brevity
}
// ── Global naming strategy via application.properties ─────────
// spring.jackson.property-naming-strategy=SNAKE_CASE
// Applies to ALL POJOs — no per-class annotation needed
// ── @JsonProperty for deserialization alias ────────────────────
// Accept both "userName" and "user_name" in incoming JSON
public class LoginRequest {
@JsonProperty("user_name") // accepts "user_name" in JSON
private String userName; // Java field stays camelCase
private String password;
// Getters/setters
public String getUserName() { return userName; }
public void setUserName(String userName) { this.userName = userName; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
}
// ── @JsonAlias — accept multiple JSON key names ────────────────
import com.fasterxml.jackson.annotation.JsonAlias;
public class SearchRequest {
@JsonAlias({"q", "query", "search_term"})
private String searchTerm; // matches any of the three JSON keys
public String getSearchTerm() { return searchTerm; }
public void setSearchTerm(String searchTerm) { this.searchTerm = searchTerm; }
}@JsonProperty applies to both serialization (field name in output JSON) and deserialization (JSON key that maps to this field). Use it when you need a single field to have a different JSON representation. The global naming strategy via spring.jackson.property-naming-strategy converts all field names uniformly — useful for APIs that must follow snake_case conventions (common in Python-based clients) while keeping Java camelCase internally. @JsonAlias provides read-only aliases and does not affect serialization output.
Excluding Fields with @JsonIgnore and @JsonIgnoreProperties
@JsonIgnore excludes a single field from both serialization and deserialization. @JsonIgnoreProperties is a class-level annotation that lists multiple fields to ignore, or sets ignoreUnknown = true to silently discard JSON keys that have no corresponding Java field — preventing UnrecognizedPropertyException when external APIs evolve.
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
// ── @JsonIgnore — exclude a single sensitive field ─────────────
public class User {
private Long id;
private String email;
@JsonIgnore // never appears in JSON output or is read from JSON
private String passwordHash;
@JsonIgnore // internal metadata, not part of the public API
private String internalNote;
// Getters/setters...
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getPasswordHash() { return passwordHash; }
public void setPasswordHash(String h) { this.passwordHash = h; }
}
// Serialized: {"id":1,"email":"alice@example.com"}
// passwordHash and internalNote are omitted
// ── @JsonIgnoreProperties(ignoreUnknown = true) ────────────────
// Prevents UnrecognizedPropertyException when external API
// adds new fields your DTO does not model yet
@JsonIgnoreProperties(ignoreUnknown = true)
public class ExternalApiResponse {
private String id;
private String status;
// If API adds "timestamp", "metadata", etc., Jackson ignores them
// instead of throwing a 400 error
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
}
// ── @JsonIgnoreProperties — ignore a list of fields by name ───
@JsonIgnoreProperties({"createdAt", "updatedAt", "version"})
public class PublicUserDTO {
private Long id;
private String name;
private String email;
// createdAt, updatedAt, version are excluded from JSON
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
}
// ── @JsonIgnore(false) — override parent class ignore ─────────
public class AdminUser extends User {
@JsonIgnore(false) // re-expose passwordHash for admin endpoints
@Override
public String getPasswordHash() { return super.getPasswordHash(); }
}
// ── @JsonAnySetter — capture unknown fields instead of dropping ─
import com.fasterxml.jackson.annotation.JsonAnySetter;
import java.util.HashMap;
import java.util.Map;
public class FlexibleDTO {
private String id;
private Map<String, Object> extraFields = new HashMap<>();
@JsonAnySetter
public void setExtra(String key, Object value) {
extraFields.put(key, value);
}
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public Map<String, Object> getExtraFields() { return extraFields; }
}The most critical use of @JsonIgnoreProperties(ignoreUnknown = true) is on DTOs that receive data from third-party APIs — external services routinely add new response fields without notice. Without it, a new field in the upstream response breaks your deserialization with a runtime exception. The global equivalent via spring.jackson.deserialization.fail-on-unknown-properties=false in application.properties applies this behavior to all classes without per-class annotation.
Global ObjectMapper Configuration via application.properties
Spring Boot maps spring.jackson.* property keys directly to ObjectMapper configuration calls. This is the lowest-friction way to configure Jackson because it requires no Java code and works with Spring Boot's externalized configuration system — different settings per environment via application-dev.properties, application-prod.properties, and so on.
# ── application.properties: complete Jackson configuration ──────
# ── Date/time serialization ───────────────────────────────────
# Serialize dates as ISO 8601 strings, not Unix timestamps
spring.jackson.serialization.write-dates-as-timestamps=false
# Serialize durations as ISO 8601 (PT15M), not numeric seconds
spring.jackson.serialization.write-durations-as-timestamps=false
# Use UTC for all date serialization
spring.jackson.time-zone=UTC
# Date format for java.util.Date fields (if not using java.time)
spring.jackson.date-format=yyyy-MM-dd'T'HH:mm:ss.SSSZ
# ── Null / empty field handling ────────────────────────────────
# Omit null fields from all JSON responses
spring.jackson.default-property-inclusion=non_null
# Also omit: non_empty (null + "" + [] + {}), non_absent (also Optional.empty)
# ── Field naming ───────────────────────────────────────────────
# Convert camelCase Java fields to snake_case JSON keys globally
# spring.jackson.property-naming-strategy=SNAKE_CASE
# ── Error handling ─────────────────────────────────────────────
# Do not fail on unknown JSON keys (safe for external APIs)
spring.jackson.deserialization.fail-on-unknown-properties=false
# Fail if JSON is empty (useful for strict request body validation)
spring.jackson.deserialization.fail-on-null-for-primitives=true
# ── Serialization features ─────────────────────────────────────
# Indent output for debugging (disable in production for size)
# spring.jackson.serialization.indent-output=true
# Sort map keys alphabetically for deterministic output
spring.jackson.serialization.order-map-entries-by-keys=true
# ── Enum serialization ─────────────────────────────────────────
# Serialize enums as their .name() string by default (already default)
# To serialize as ordinal integer: write-enums-using-index=true
# ── application.yml equivalent ────────────────────────────────
# spring:
# jackson:
# serialization:
# write-dates-as-timestamps: false
# deserialization:
# fail-on-unknown-properties: false
# default-property-inclusion: non_null
# time-zone: UTCThe full list of configurable features is documented under SerializationFeature, DeserializationFeature, and MapperFeature enums in the Jackson API. Each enum constant maps to a lowercase hyphenated property key under spring.jackson.serialization.*, spring.jackson.deserialization.*, and spring.jackson.mapper.* respectively. Spring Boot's JacksonProperties source class lists all supported keys. For production APIs, the most impactful settings are write-dates-as-timestamps=false (prevents numeric date confusion) and default-property-inclusion=non_null (reduces payload size).
Writing Custom JsonSerializer and JsonDeserializer
Custom serializers and deserializers let you control the exact JSON representation for any Java type. Extend JsonSerializer<T> and override serialize() to control output; extend JsonDeserializer<T> and override deserialize() to parse custom input. Register them via @JsonSerialize/@JsonDeserialize on a field or via a Jackson Module registered in the ObjectMapper.
import com.fasterxml.jackson.core.*;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.annotation.*;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.RoundingMode;
// ── Custom Serializer: BigDecimal → 2 decimal places ──────────
public class MoneySerializer extends JsonSerializer<BigDecimal> {
@Override
public void serialize(BigDecimal value, JsonGenerator gen,
SerializerProvider provider) throws IOException {
// Always output exactly 2 decimal places: 29.9 → "29.90"
gen.writeString(value.setScale(2, RoundingMode.HALF_UP).toPlainString());
}
}
// ── Custom Deserializer: string "29.90" → BigDecimal ──────────
public class MoneyDeserializer extends JsonDeserializer<BigDecimal> {
@Override
public BigDecimal deserialize(JsonParser p, DeserializationContext ctx)
throws IOException {
String text = p.getText().trim();
if (text.isEmpty()) return BigDecimal.ZERO;
// Strip currency symbols before parsing
return new BigDecimal(text.replaceAll("[^0-9.]", ""));
}
}
// ── Apply per-field ────────────────────────────────────────────
public class PriceDTO {
@JsonSerialize(using = MoneySerializer.class)
@JsonDeserialize(using = MoneyDeserializer.class)
private BigDecimal price;
public BigDecimal getPrice() { return price; }
public void setPrice(BigDecimal price) { this.price = price; }
}
// Serialized: {"price":"29.90"}
// Deserialized: "$29.90" → BigDecimal(29.90)
// ── Register via a Jackson Module (global scope) ───────────────
import com.fasterxml.jackson.databind.module.SimpleModule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class JacksonModuleConfig {
@Bean
public SimpleModule moneyModule() {
SimpleModule module = new SimpleModule("MoneyModule");
module.addSerializer(BigDecimal.class, new MoneySerializer());
module.addDeserializer(BigDecimal.class, new MoneyDeserializer());
return module;
}
// Spring Boot auto-detects SimpleModule beans and registers them
}
// ── Contextual Serializer: adapt based on annotation ──────────
import com.fasterxml.jackson.databind.ser.ContextualSerializer;
import java.lang.annotation.*;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Redact {} // custom annotation
public class RedactSerializer extends JsonSerializer<String>
implements ContextualSerializer {
private boolean redact = false;
@Override
public JsonSerializer<?> createContextual(SerializerProvider prov,
BeanProperty property) {
if (property != null && property.getAnnotation(Redact.class) != null) {
RedactSerializer s = new RedactSerializer();
s.redact = true;
return s;
}
return this;
}
@Override
public void serialize(String value, JsonGenerator gen,
SerializerProvider provider) throws IOException {
gen.writeString(redact ? "***REDACTED***" : value);
}
}
// Use: @Redact @JsonSerialize(using = RedactSerializer.class)
// private String creditCardNumber;The SimpleModule approach is cleaner than per-field annotations for types used throughout the application — register it once and it applies everywhere that type appears. For types from external libraries (like java.time.Instant or org.joda.time.DateTime), register the corresponding Jackson module (JavaTimeModule, JodaModule) rather than writing custom serializers, since those modules are well-tested and handle edge cases including null values, timezones, and serialization features.
Polymorphic JSON with @JsonTypeInfo and @JsonSubTypes
Polymorphic serialization embeds type information in JSON so that Jackson can reconstruct the correct subclass on deserialization. @JsonTypeInfo controls how type information is included (as a property, wrapper array, or wrapper object); @JsonSubTypes maps type identifier strings to concrete classes. This pattern is common for event-driven systems, command/event sourcing, and APIs with heterogeneous response shapes.
import com.fasterxml.jackson.annotation.*;
// ── Base class with @JsonTypeInfo ──────────────────────────────
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME, // include type as a string name
include = JsonTypeInfo.As.PROPERTY, // add a "type" property to JSON
property = "type" // JSON key: "type"
)
@JsonSubTypes({
@JsonSubTypes.Type(value = ClickEvent.class, name = "click"),
@JsonSubTypes.Type(value = KeypressEvent.class, name = "keypress"),
@JsonSubTypes.Type(value = ResizeEvent.class, name = "resize"),
})
public abstract class UserEvent {
private long timestamp;
private String userId;
public long getTimestamp() { return timestamp; }
public void setTimestamp(long timestamp) { this.timestamp = timestamp; }
public String getUserId() { return userId; }
public void setUserId(String userId) { this.userId = userId; }
}
// ── Concrete subclasses ────────────────────────────────────────
public class ClickEvent extends UserEvent {
private int x;
private int y;
private String button; // "left", "right", "middle"
public int getX() { return x; }
public void setX(int x) { this.x = x; }
public int getY() { return y; }
public void setY(int y) { this.y = y; }
public String getButton() { return button; }
public void setButton(String button) { this.button = button; }
}
public class KeypressEvent extends UserEvent {
private String key;
private boolean ctrlKey;
public String getKey() { return key; }
public void setKey(String key) { this.key = key; }
public boolean isCtrlKey() { return ctrlKey; }
public void setCtrlKey(boolean ctrlKey) { this.ctrlKey = ctrlKey; }
}
public class ResizeEvent extends UserEvent {
private int width;
private int height;
public int getWidth() { return width; }
public void setWidth(int width) { this.width = width; }
public int getHeight() { return height; }
public void setHeight(int height) { this.height = height; }
}
// ── Controller using polymorphic types ─────────────────────────
@RestController
@RequestMapping("/api/events")
public class EventController {
// POST body: {"type":"click","x":100,"y":200,"button":"left","timestamp":1748390400000,"userId":"u1"}
// Jackson reads "type":"click" and deserializes into ClickEvent
@PostMapping
public ResponseEntity<UserEvent> logEvent(@RequestBody UserEvent event) {
// instanceof check to branch on subtype
if (event instanceof ClickEvent click) {
System.out.println("Click at " + click.getX() + "," + click.getY());
}
eventService.store(event);
return ResponseEntity.status(201).body(event);
}
// GET returns list of mixed event types — each has a "type" key
@GetMapping
public List<UserEvent> getEvents() {
return eventService.findAll();
}
}
// ── JsonTypeInfo.As.WRAPPER_OBJECT alternative ────────────────
// Wraps entire object in a keyed object:
// {"click":{"x":100,"y":200,...}} instead of {"type":"click","x":100,...}
// Less common but avoids the "type" key collision with domain fieldsJsonTypeInfo.Id.NAME with JsonTypeInfo.As.PROPERTY is the most readable and common configuration — the type discriminant appears as a regular JSON key. JsonTypeInfo.Id.CLASS embeds the fully qualified Java class name, which is convenient but ties JSON to your package structure and creates a security risk (deserialization gadget attacks). Always prefer JsonTypeInfo.Id.NAME with an explicit @JsonSubTypes allowlist for safe deserialization of external JSON. Jackson 2.x also supports @JsonTypeIdResolver for custom type resolution logic when the discriminant mapping is too complex for a static annotation.
FAQ
How does Spring Boot serialize Java objects to JSON automatically?
Spring Boot includes jackson-databind on the classpath via spring-boot-starter-web and auto-configures a Jackson ObjectMapper bean through JacksonAutoConfiguration. Any method in a @RestController that returns a Java object triggers MappingJackson2HttpMessageConverter, which serializes the return value using the ObjectMapper and sets Content-Type: application/json. Jackson maps Java field names to JSON keys using camelCase by default — a field named firstName becomes "firstName" in JSON. For a 10,000-element List, Jackson serializes it in approximately 8 ms on a modern JVM. No XML configuration or manual ObjectMapper.writeValueAsString() calls are needed.
How do I customize Jackson ObjectMapper in Spring Boot?
There are three approaches with increasing specificity. First, use application.properties keys under the spring.jackson.* namespace — for example, spring.jackson.default-property-inclusion=non_null omits null fields globally and spring.jackson.serialization.write-dates-as-timestamps=false serializes Java dates as ISO 8601 strings. Second, declare a Jackson2ObjectMapperBuilderCustomizer bean to adjust the builder before it creates the ObjectMapper — this is the recommended approach when you need to register modules or set serialization features while still benefiting from Spring Boot's 30+ defaults. Third, declare your own ObjectMapper bean to take full control, but be aware this replaces Spring Boot's auto-configuration entirely.
How do I handle null fields in Spring Boot JSON responses?
Set spring.jackson.default-property-inclusion=non_null in application.properties to omit all null fields globally across every @RestController response. Alternatively, annotate a specific class with @JsonInclude(JsonInclude.Include.NON_NULL) to apply the policy only to that class's fields. Use @JsonInclude(JsonInclude.Include.NON_EMPTY) to also omit empty strings, empty collections, and empty maps. The global application.properties setting maps to the Jackson Include enum value names: NON_NULL, NON_EMPTY, NON_ABSENT, ALWAYS, NON_DEFAULT. NON_NULL is the most common choice for REST APIs — a typical 10-field POJO with 3 null fields shrinks by roughly 30% of its key/value pairs.
What is the difference between @JsonIgnore and @JsonIgnoreProperties?
@JsonIgnore is a field- or method-level annotation that excludes that specific field from both serialization (JSON output) and deserialization (JSON input). If a JSON payload includes the field, Jackson ignores it on read; if the Java object has the field, Jackson omits it on write. @JsonIgnoreProperties is a class-level annotation that either lists multiple field names to ignore by name, or sets ignoreUnknown = true to silently discard any JSON keys that do not map to a Java field — preventing UnrecognizedPropertyException. As a rule: use @JsonIgnore for single sensitive fields like passwords, and use @JsonIgnoreProperties(ignoreUnknown = true) on DTOs that consume external APIs where the response schema may add new fields at any time.
How do I deserialize JSON with unknown fields in Spring Boot?
Add @JsonIgnoreProperties(ignoreUnknown = true) to the target Java class. Without this, Jackson throws UnrecognizedPropertyException (HTTP 400) when the JSON payload contains a key that has no matching field in the POJO. Alternatively, set spring.jackson.deserialization.fail-on-unknown-properties=false in application.properties to disable this check globally — useful during development when upstream APIs are evolving. A more targeted approach is to use @JsonAnySetter on a Map<String, Object> field to capture unknown properties for later inspection rather than discarding them. For strict security-sensitive endpoints where unexpected fields should fail fast, keep the default behavior and rely on @JsonIgnoreProperties selectively rather than a global override.
How do I return a custom JSON error response in Spring Boot?
Spring Boot 3.x supports RFC 9457 Problem Details out of the box — enable it with spring.mvc.problemdetails.enabled=true. For full control, create a @RestControllerAdvice class with @ExceptionHandler methods that return ResponseEntity<ErrorResponse> where ErrorResponse is your custom POJO — Jackson serializes it to JSON automatically. For @RequestBody validation failures, Spring throws MethodArgumentNotValidException; catch it in @ExceptionHandler to extract BindingResult field errors and return a structured 422 body with all 1 or more failed constraint messages. Spring Boot's auto-configured BasicErrorController returns a default JSON error body for unmapped exceptions with fields: timestamp, status, error, message, path.
How do I serialize Java dates to ISO 8601 in Spring Boot JSON?
Set spring.jackson.serialization.write-dates-as-timestamps=false in application.properties. Without this, Jackson serializes java.util.Date and java.time.LocalDateTime as Unix timestamps (numeric milliseconds), which is harder for clients to parse. With this setting, Jackson produces ISO 8601 strings: "2026-05-28T10:00:00.000Z" for ZonedDateTime/Instant, "2026-05-28T10:00:00" for LocalDateTime. The JavaTimeModule is auto-registered by Spring Boot when jackson-datatype-jsr310 is on the classpath (it is, via spring-boot-starter-web). Set spring.jackson.time-zone=UTC so all serialized datetimes use the same offset. Use @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd") on a field to override the global format for that specific field.
How do I validate @RequestBody JSON in Spring Boot?
Add @Valid or @Validated to the @RequestBody parameter. Spring Boot auto-configures Bean Validation (Hibernate Validator) via spring-boot-starter-validation. Annotate the POJO fields with Jakarta Validation constraints: @NotNull, @NotBlank, @Size(min=1, max=255), @Email, @Min, @Max, @Pattern. When validation fails, Spring throws MethodArgumentNotValidException with a BindingResult containing all 1 or more constraint violations. Catch it in a @RestControllerAdvice @ExceptionHandler to return a 422 response with field errors. For nested objects, add @Valid on the nested field for recursive validation. For cross-field validation (e.g., start date before end date), create a custom constraint annotation with a ConstraintValidator<A, T> implementation.
Format and validate Spring Boot JSON responses
Paste any JSON from your Spring Boot API into Jsonic's formatter to pretty-print, validate, and inspect the structure instantly.
Open JSON FormatterFurther reading and primary sources
- Spring Boot JSON Documentation — Official Spring Boot reference for JSON support, Jackson auto-configuration, and Gson/JSON-B alternatives
- Jackson Annotations Documentation — Complete reference for @JsonProperty, @JsonIgnore, @JsonTypeInfo, and all other Jackson annotations
- Jackson Databind GitHub — Jackson core databind library source, issues, and serialization/deserialization documentation
- Spring MVC @RequestBody / @ResponseBody — Spring Framework reference for @RequestBody, @ResponseBody, and HttpMessageConverter lifecycle
- Bean Validation with Spring Boot — Spring Boot guide for @Valid, @Validated, and Jakarta Bean Validation constraints on @RequestBody