Spring Boot
JSON Handling
Spring Boot uses Jackson as its default JSON library, auto-configured through spring-boot-starter-web. This entry covers Jackson configuration, custom serializers/deserializers, date/time handling, ignoring unknown fields, renaming fields, polymorphic types, and common pitfalls when shaping JSON for REST APIs.
Jackson Auto-Configuration
Spring Boot auto-configures a single ObjectMapper bean via JacksonAutoConfiguration. Declare a Jackson2ObjectMapperBuilderCustomizer bean to tune it globally — or replace it entirely by registering your own ObjectMapper bean. Never create a second ObjectMapper manually; always customize the one Spring manages.
Java
// ── Option 1: Customise via builder (preferred) ───────────────────────
@Configuration
public class JacksonConfig {
@Bean
public Jackson2ObjectMapperBuilderCustomizer jacksonCustomizer() {
return builder -> builder
.featuresToDisable(
SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, // ISO-8601 dates
DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES // tolerate extra fields
)
.featuresToEnable(
MapperFeature.DEFAULT_VIEW_INCLUSION
)
.modules(new JavaTimeModule()) // java.time support
.timeZone(TimeZone.getTimeZone("UTC"));
}
}
// ── Option 2: application.yml (no code needed for common settings) ─────
// spring:
// jackson:
// serialization:
// write-dates-as-timestamps: false
// deserialization:
// fail-on-unknown-properties: false
// default-property-inclusion: non_null
// time-zone: UTC
// ── Option 3: Replace the ObjectMapper entirely ────────────────────────
@Bean
@Primary
public ObjectMapper objectMapper() {
return JsonMapper.builder()
.addModule(new JavaTimeModule())
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.serializationInclusion(JsonInclude.Include.NON_NULL)
.build();
}Shaping JSON with Jackson Annotations
Jackson annotations let you control exactly how Java fields map to JSON keys — without changing your domain model. The most commonly used ones are @JsonProperty, @JsonIgnore, @JsonInclude, @JsonAlias, and @JsonNaming.
Java
// ── Rename a field ────────────────────────────────────────────────────
public record UserResponse(
@JsonProperty("user_id") Long id, // serializes as "user_id"
@JsonProperty("full_name") String name,
String email
) {}
// ── Ignore a field ────────────────────────────────────────────────────
public class UserEntity {
private Long id;
private String name;
@JsonIgnore // never appears in JSON output or input
private String passwordHash;
}
// ── Exclude null / empty values ───────────────────────────────────────
@JsonInclude(JsonInclude.Include.NON_NULL) // omit null fields for this class
public record ProductResponse(
Long id,
String name,
String description, // omitted from JSON when null
BigDecimal discount // omitted from JSON when null
) {}
// ── Accept multiple input names (aliases) ─────────────────────────────
public record CreateUserRequest(
@JsonAlias({"full_name", "fullName", "name"}) String name,
String email
) {}
// ── Snake_case for the whole class ────────────────────────────────────
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public record OrderResponse(
Long orderId, // → "order_id"
LocalDateTime createdAt // → "created_at"
) {}
// ── Flatten a nested object ───────────────────────────────────────────
public class AddressResponse {
@JsonUnwrapped // street/city/zip appear at the top level
private Address address;
private String phone;
}Date and Time Handling
The JavaTimeModule (from jackson-datatype-jsr310, included in spring-boot-starter-web) handles java.time types. Disable WRITE_DATES_AS_TIMESTAMPS to get ISO-8601 strings instead of epoch arrays. Use @JsonFormat on individual fields for per-field patterns.
Java
// ── Global config (application.yml) ──────────────────────────────────
// spring:
// jackson:
// serialization:
// write-dates-as-timestamps: false # "2024-03-15T10:30:00Z" not [2024,3,15,...]
// time-zone: UTC
// ── DTO with java.time fields ─────────────────────────────────────────
public record EventResponse(
Long id,
String title,
@JsonFormat(shape = JsonFormat.Shape.STRING,
pattern = "yyyy-MM-dd",
timezone = "UTC")
LocalDate eventDate, // → "2024-03-15"
@JsonFormat(shape = JsonFormat.Shape.STRING,
pattern = "yyyy-MM-dd'T'HH:mm:ss'Z'",
timezone = "UTC")
LocalDateTime createdAt, // → "2024-03-15T10:30:00Z"
Instant updatedAt // → "2024-03-15T10:30:00Z" (Instant is ISO by default)
) {}
// ── Deserializing dates from a request body ───────────────────────────
public record CreateEventRequest(
String title,
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd")
LocalDate eventDate, // accepts "2024-03-15" in request JSON
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
LocalDateTime startsAt
) {}Custom Serializers and Deserializers
When annotations are not enough — for example, to format a BigDecimal as a currency string, or to parse a non-standard date format — write a custom JsonSerializer or JsonDeserializer and register it on the field or globally via a Jackson Module.
Java
// ── Custom serializer: BigDecimal → "$1,234.56" ───────────────────────
public class MoneySerializer extends JsonSerializer<BigDecimal> {
private static final NumberFormat FMT =
NumberFormat.getCurrencyInstance(Locale.US);
@Override
public void serialize(BigDecimal value, JsonGenerator gen,
SerializerProvider provider) throws IOException {
gen.writeString(FMT.format(value));
}
}
// ── Custom deserializer: "$1,234.56" → BigDecimal ─────────────────────
public class MoneyDeserializer extends JsonDeserializer<BigDecimal> {
@Override
public BigDecimal deserialize(JsonParser p,
DeserializationContext ctx) throws IOException {
String raw = p.getText().replaceAll("[$,]", "");
return new BigDecimal(raw);
}
}
// ── Apply on a field ──────────────────────────────────────────────────
public record PriceResponse(
String product,
@JsonSerialize(using = MoneySerializer.class)
@JsonDeserialize(using = MoneyDeserializer.class)
BigDecimal price
) {}
// ── Register globally via a Module ───────────────────────────────────
@Configuration
public class JacksonConfig {
@Bean
public Jackson2ObjectMapperBuilderCustomizer customizers() {
return builder -> builder.deserializerByType(BigDecimal.class, new MoneyDeserializer())
.serializerByType(BigDecimal.class, new MoneySerializer());
}
}Polymorphic JSON with @JsonTypeInfo
When a field can hold one of several subtypes, Jackson needs a type discriminator in the JSON. @JsonTypeInfo and @JsonSubTypes configure this. Use PROPERTY (an explicit field) or EXISTING_PROPERTY (a field already in the payload) as the inclusion strategy.
Java
// ── Base class with type discriminator ────────────────────────────────
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "type" // {"type":"card", ...} or {"type":"paypal", ...}
)
@JsonSubTypes({
@JsonSubTypes.Type(value = CardPayment.class, name = "card"),
@JsonSubTypes.Type(value = PayPalPayment.class, name = "paypal"),
@JsonSubTypes.Type(value = BankPayment.class, name = "bank")
})
public abstract class Payment {
public abstract BigDecimal amount();
}
// ── Subtypes ──────────────────────────────────────────────────────────
public record CardPayment(
BigDecimal amount,
String cardNumber,
String expiry
) extends Payment {}
public record PayPalPayment(
BigDecimal amount,
String email
) extends Payment {}
// ── Controller accepts polymorphic request body ───────────────────────
@PostMapping("/payments")
public ResponseEntity<Void> pay(@RequestBody @Valid Payment payment) {
// payment is a CardPayment or PayPalPayment at runtime
paymentService.process(payment);
return ResponseEntity.accepted().build();
}
// ── Example request bodies:
// { "type": "card", "amount": 99.99, "cardNumber": "4111...", "expiry": "12/26" }
// { "type": "paypal", "amount": 49.00, "email": "user@example.com" }JSON Views for Partial Responses
@JsonView lets a single DTO serialize different subsets of fields depending on the context — for example, returning fewer fields in a list endpoint than in a detail endpoint. Define marker interfaces as view names and annotate fields with the views they belong to.
Java
// ── Define view marker interfaces ─────────────────────────────────────
public class UserViews {
public interface Summary {} // minimal: id + name
public interface Detail extends Summary {} // everything
public interface Admin extends Detail {} // includes sensitive fields
}
// ── Annotate DTO fields ───────────────────────────────────────────────
public class UserResponse {
@JsonView(UserViews.Summary.class)
public Long id;
@JsonView(UserViews.Summary.class)
public String name;
@JsonView(UserViews.Detail.class)
public String email;
@JsonView(UserViews.Detail.class)
public LocalDateTime createdAt;
@JsonView(UserViews.Admin.class)
public String internalNotes; // only visible in Admin view
}
// ── Use @JsonView in the controller ──────────────────────────────────
@GetMapping
@JsonView(UserViews.Summary.class) // list returns id + name only
public List<UserResponse> findAll() {
return userService.findAll();
}
@GetMapping("/{id}")
@JsonView(UserViews.Detail.class) // detail returns everything except admin fields
public UserResponse findById(@PathVariable Long id) {
return userService.findById(id);
}