Mastering Spring Boot 3 Jackson Annotations: 25+ Core Annotations and Production-Ready Practices

This comprehensive guide walks you through why Jackson annotation tweaks are essential, presents a full annotation hierarchy, demonstrates over 25 core annotations with concrete code examples, covers polymorphic handling, special scenarios, global Spring Boot 3 configuration, production best practices, common pitfalls, performance tips, an end‑to‑end e‑commerce example, and testing strategies, all aimed at mastering JSON serialization in Spring Boot.

Cloud Architecture
Cloud Architecture
Cloud Architecture
Mastering Spring Boot 3 Jackson Annotations: 25+ Core Annotations and Production-Ready Practices

Why You Need This Jackson Annotation Guide

Typical front‑end complaints include field names that are too long, null fields that should be omitted, and date formats that differ from expectations. Additionally, circular references can cause StackOverflowError. This article addresses all those pain points.

Jackson Annotation Hierarchy

Jackson Annotations
├── Field control
│   ├── @JsonProperty
│   ├── @JsonIgnore
│   ├── @JsonInclude
│   └── @JsonFormat
├── Class control
│   ├── @JsonIgnoreProperties
│   ├── @JsonInclude
│   └── @JsonAutoDetect
├── Deserialization
│   ├── @JsonDeserialize
│   ├── @JsonCreator
│   └── @JacksonInject
├── Polymorphism
│   ├── @JsonTypeInfo
│   └── @JsonSubTypes
├── Special scenarios
│   ├── @JsonView
│   ├── @JsonUnwrapped
│   └── @JsonManagedReference / @JsonBackReference
└── Custom
    └── @JsonSerialize

Core Annotations with Practical Code

@JsonProperty – Field Name Mapping

public class UserDTO {
    @JsonProperty("user_id")
    private Long userId;

    @JsonProperty(value = "password", access = JsonProperty.Access.WRITE_ONLY)
    private String password;

    @JsonProperty(value = "internalCode", access = JsonProperty.Access.READ_ONLY)
    private String internalCode;
}

@JsonIgnore – Ignoring Fields

public class UserDTO {
    private Long id;
    private String name;

    @JsonIgnore
    private String secretKey; // never serialized or deserialized

    @JsonIgnore
    public String getInternalInfo() { return "internal"; } // ignored on serialization only
}

@JsonInclude – Controlling Null Output

@JsonInclude(JsonInclude.Include.NON_NULL)
public class UserDTO {
    private Long id;
    private String name;
    private String email; // omitted when null
    private List<String> tags; // omitted when empty
    private Integer age = 0; // omitted when default value
    private String status; // always output, even if null
    private Optional<String> nickname; // omitted when empty
}

@JsonFormat – Formatting Output

public class OrderDTO {
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private LocalDateTime createTime;

    @JsonFormat(shape = JsonFormat.Shape.STRING)
    private Long orderId; // serialized as string

    @JsonFormat(pattern = "#,#00.00")
    private BigDecimal amount; // e.g., "1,234.56"
}

@JsonIgnoreProperties – Class‑Level Ignoring

@JsonIgnoreProperties({"secretKey", "internalCode"})
public class UserDTO {
    private Long id;
    private String name;
    private String secretKey; // ignored
    private String internalCode; // ignored
}

@JsonIgnoreProperties(ignoreUnknown = true)
public class UserDTO {
    private Long id;
    private String name;
    // unknown fields from the client are ignored during deserialization
}

@JsonDeserialize – Custom Deserializer

public class OrderDTO {
    @JsonDeserialize(using = CustomDateDeserializer.class)
    private LocalDateTime createTime;

    @JsonDeserialize(contentUsing = CustomItemDeserializer.class)
    private List<Item> items;
}

public class CustomDateDeserializer extends JsonDeserializer<LocalDateTime> {
    private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
    @Override
    public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
        return LocalDateTime.parse(p.getText(), FORMATTER);
    }
}

@JsonSerialize – Custom Serializer (e.g., Data Masking)

public class UserDTO {
    @JsonSerialize(using = SensitiveSerializer.class)
    private String phone; // masked as 138****1234
}

public class SensitiveSerializer extends JsonSerializer<String> {
    @Override
    public void serialize(String value, JsonGenerator gen, SerializerProvider provider) throws IOException {
        if (value == null) { gen.writeNull(); return; }
        String masked = value.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");
        gen.writeString(masked);
    }
}

Polymorphic Handling

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")
@JsonSubTypes({
    @JsonSubTypes.Type(value = Dog.class, name = "dog"),
    @JsonSubTypes.Type(value = Cat.class, name = "cat"),
    @JsonSubTypes.Type(value = Bird.class, name = "bird")
})
public abstract class Animal {
    private String name;
    private Integer age;
}

public class Dog extends Animal { private String breed; private Boolean canGuard; }
public class Cat extends Animal { private Boolean isIndoor; private String color; }
public class Bird extends Animal { private Double wingspan; private Boolean canFly; }

Serialization example:

{"type":"dog","name":"旺财","age":3,"breed":"金毛","canGuard":true}

Special Scenario Annotations

@JsonView – View Control

public class Views {
    public static class Public {}
    public static class Internal extends Public {}
    public static class Admin extends Internal {}
}

public class UserDTO {
    @JsonView(Views.Public.class)
    private Long id;
    @JsonView(Views.Public.class)
    private String name;
    @JsonView(Views.Internal.class)
    private String email;
    @JsonView(Views.Admin.class)
    private String passwordHash;
    @JsonView(Views.Admin.class)
    private LocalDateTime lastLoginTime;
}

@JsonUnwrapped – Flattening Nested Objects

public class Address {
    private String province;
    private String city;
    private String district;
    private String street;
}

public class UserDTO {
    private Long id;
    private String name;
    @JsonUnwrapped
    private Address address;
}

Result without @JsonUnwrapped nests the address; with it, address fields appear at the top level.

@JsonManagedReference / @JsonBackReference – Solving Circular References

public class Order {
    private Long id;
    private String orderNo;
    @JsonManagedReference
    private List<OrderItem> items;
}

public class OrderItem {
    private Long id;
    private String productName;
    private Integer quantity;
    @JsonBackReference
    private Order order; // omitted during serialization to break the cycle
}

Spring Boot 3 Global Jackson Configuration

application.yml

spring:
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: GMT+8
    serialization:
      write-null-values: false
      write-dates-as-timestamps: false
    deserialization:
      fail-on-unknown-properties: false
      accept-single-value-as-array: true
    default-property-inclusion: non_null
    mapper:
      use-tostring-for-enum-key: true

Java Config

@Configuration
public class JacksonConfig {
    @Bean
    public Jackson2ObjectMapperBuilderCustomizer customizer() {
        return builder -> builder
            .failOnUnknownProperties(false)
            .serializationInclusion(JsonInclude.Include.NON_NULL)
            .propertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE)
            .featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
            .simpleDateFormat("yyyy-MM-dd HH:mm:ss")
            .timeZone(TimeZone.getTimeZone("GMT+8"))
            .modules(new JavaTimeModule());
    }
}

Production‑Level Best Practices

DTO Design

@JsonInclude(JsonInclude.Include.NON_NULL)
public class UserResponse { private Long id; private String name; private String email; }

public class CreateUserRequest {
    @JsonProperty(required = true) private String username;
    @JsonProperty(required = true) private String password;
    private String nickname;
}

Sensitive Data Masking

@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@JsonSerialize(using = SensitiveSerializer.class)
public @interface Sensitive { SensitiveType value(); }

public enum SensitiveType { PHONE, ID_CARD, EMAIL, ADDRESS, BANK_CARD }

Unified Time Formatting

@Configuration
public class DateTimeConfig {
    @Bean
    public Module javaTimeModule() {
        JavaTimeModule module = new JavaTimeModule();
        DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        module.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(dtf));
        module.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(dtf));
        return module;
    }
}

Enum Handling

public enum OrderStatus {
    PENDING("待支付"), PAID("已支付"), SHIPPED("已发货"), COMPLETED("已完成"), CANCELLED("已取消");
    private final String description;
    OrderStatus(String description) { this.description = description; }
    @JsonValue
    public String getDescription() { return description; }
    @JsonCreator
    public static OrderStatus fromDescription(String desc) {
        for (OrderStatus s : values()) if (s.description.equals(desc)) return s;
        throw new IllegalArgumentException("Unknown status: " + desc);
    }
}

Common Issues & Solutions

Date format inconsistency: Use class‑level @JsonFormat, global application.yml date‑format, or a custom JavaTimeModule.

Too many null fields: Apply @JsonInclude(JsonInclude.Include.NON_NULL) at class level or configure default-property-inclusion: non_null globally.

Circular reference StackOverflowError: Prefer @JsonManagedReference / @JsonBackReference or @JsonIdentityInfo for ID‑based references.

Generic deserialization loss: Use new TypeReference<Result<UserDTO>>() {} or construct a JavaType via ObjectMapper.getTypeFactory().

Performance Optimizations

Reuse a singleton ObjectMapper instead of creating a new instance per request.

For high concurrency, pool ObjectMapper instances with Apache Commons Pool.

Process large JSON files with Jackson streaming API ( JsonParser) to avoid loading the whole document into memory.

End‑to‑End E‑Commerce Example (Spring Boot 3)

DTO Design

@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public record OrderDetailResponse(
    Long orderId,
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") LocalDateTime payTime,
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") LocalDateTime deliverTime,
    BigDecimal amount,
    @JsonSerialize(using = SensitiveSerializer.class) String buyerPhone,
    List<OrderItemView> items,
    Map<String, Object> extra) {}

Controller Snippet

@RestController
@RequestMapping("/api/orders")
public class OrderController {
    private final OrderService orderService;
    private final ObjectMapper objectMapper; // reused globally
    public OrderController(OrderService orderService, ObjectMapper objectMapper) {
        this.orderService = orderService;
        this.objectMapper = objectMapper;
    }
    @GetMapping("/{id}")
    public OrderDetailResponse detail(@PathVariable Long id) {
        return orderService.findDetail(id);
    }
    @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<Void> create(@RequestBody CreateOrderRequest req) {
        orderService.create(req);
        return ResponseEntity.status(HttpStatus.CREATED).build();
    }
}

Sample Request & Response

{
  "user_id": 1001,
  "items": [{"sku_id": 90001, "quantity": 2}],
  "buyer_phone": "13812341234",
  "extra": {"channel": "h5"}
}

// Response
{
  "order_id": 202403210001,
  "pay_time": "2024-03-21 10:30:00",
  "amount": 199.00,
  "buyer_phone": "138****1234",
  "items": [{"sku_id": 90001, "sku_name": "蓝牙耳机", "quantity": 2, "price": "99.50"}]
}

Integration with Lombok & Records

@Data @Builder @JsonDeserialize(builder = UserDTO.UserDTOBuilder.class)
public class UserDTO {
    private Long id;
    private String name;
    @JsonPOJOBuilder(withPrefix = "")
    public static class UserDTOBuilder {}
}

public record PaymentRequest(
    @JsonProperty(value = "order_id", required = true) Long orderId,
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") LocalDateTime payTime,
    @JsonAlias({"amt", "price"}) BigDecimal amount) {}

Testing with @JsonTest

@JsonTest
class OrderDetailResponseTest {
    @Autowired
    private JacksonTester<OrderDetailResponse> json;

    @Test
    void serialize_shouldMaskPhone() throws Exception {
        OrderDetailResponse resp = new OrderDetailResponse(
            1L,
            LocalDateTime.of(2024,3,21,10,30),
            null,
            new BigDecimal("199.00"),
            "13812341234",
            List.of(new OrderItemView(90001L, "耳机", 2, new BigDecimal("99.50"))),
            Map.of()
        );
        assertThat(json.write(resp)).extractingJsonPathStringValue("$.buyer_phone")
            .isEqualTo("138****1234");
        assertThat(json.write(resp)).hasJsonPathValue("$.pay_time");
    }
}

Production Checklist

Ensure WRITE_DATES_AS_TIMESTAMPS is disabled to avoid numeric timestamps.

Add @JsonPOJOBuilder(withPrefix = "") when using Lombok @Builder for proper deserialization.

Use @JsonAlias for backward‑compatible field names during migrations.

Prefer @JsonIdentityInfo for complex object graphs; otherwise, @JsonIgnore can break cycles.

Apply @JsonInclude(JsonInclude.Include.NON_ABSENT) to suppress empty Optional fields.

Verify that custom Mix‑In modules are registered on the same ObjectMapper instance used by Spring.

Mastering these annotations gives you full control over API payloads, improves client compatibility, and reduces bugs in production.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

backendJavaJSONspring-bootannotationsJackson
Cloud Architecture
Written by

Cloud Architecture

Focuses on cloud‑native and distributed architecture engineering, sharing practical solutions and lessons learned. Covers microservice governance, Kubernetes, observability, and stability engineering to help your systems run stable, fast, and cost‑effectively.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.