Spring Boot cấu hình Jackson sẵn, và phần lớn mặc định là hợp lý. Bài này về những chỗ cần biết.

JSON ra

record Nguoi(String ten, Integer tuoi,
             @JsonIgnore String bimat,
             @JsonProperty("ngay_sinh") LocalDate ngaySinh,
             Instant taoLuc,
             @JsonInclude(NON_NULL) String ghiChu) {}
  {"ten":"Minh","tuoi":null,"taoLuc":"2026-08-13T10:00:00Z","ngay_sinh":"1995-03-15"}

Bốn điều đọc ra được:

bimat biến mất@JsonIgnore. Dùng cho mật khẩu, token, mọi thứ không được ra ngoài.

ghiChu biến mất vì null và có @JsonInclude(NON_NULL); còn tuoi vẫn hiện là null vì không có chú thích đó. Muốn bỏ mọi trường null trên toàn ứng dụng:

spring:
  jackson:
    default-property-inclusion: non_null

ngay_sinh đổi tên nhờ @JsonProperty. Đổi cho cả ứng dụng thì dùng spring.jackson.property-naming-strategy: SNAKE_CASE.

java.time hoạt động sẵn. Instant thành 2026-08-13T10:00:00Z, LocalDate thành 1995-03-15 — đúng ISO-8601.

Đây là điểm Spring Boot làm hộ: nó đăng ký JavaTimeModule và tắt WRITE_DATES_AS_TIMESTAMPS. Tự tạo ObjectMapper bằng new thì mất cả hai, và bạn nhận về một con số dấu phẩy động — đúng vấn đề ở bài 60 sê-ri Java.

record ánh xạ không cần chú thích. Jackson đọc tên thành phần trực tiếp.

JSON vào

  gửi {"ten":"Lan","ngay_sinh":"2000-01-02","khong_co":123}
  nhận ten=Lan  ngaySinh=2000-01-02  tuoi=null

Hai hành vi im lặng:

Trường lạ bị bỏ qua. Spring Boot tắt FAIL_ON_UNKNOWN_PROPERTIES theo mặc định — khác với Jackson thuần. Với API bên ngoài thì đó là đúng; với DTO nội bộ thì nó che lỗi gõ nhầm.

Bật lại khi cần:

spring:
  jackson:
    deserialization:
      fail-on-unknown-properties: true

Trường thiếu thành zero value. Integer thành null, int thành 0. Không có cảnh báo.

Đây là lý do @Valid ở bài 13 quan trọng: Jackson không kiểm gì cả, validation mới kiểm.

Và như bài 45 sê-ri Go đã nói, dùng kiểu bọc (Integer) thay vì nguyên thuỷ (int) khi cần phân biệt "không gửi" với "gửi số 0".

Vòng lặp tham chiếu

class Cha { public String ten; public Con con; }
class Con { public String ten; public Cha cha; }   // trỏ ngược

Serialize Cha sẽ đệ quy vô hạn và ném StackOverflowError — hoặc tệ hơn, sinh JSON khổng lồ trước khi chết.

Ba cách chữa:

class Con { @JsonBackReference public Cha cha; }
  {"ten":"cha","con":{"ten":"con"}}

Vòng bị cắt. @JsonManagedReference ở phía cha, @JsonBackReference ở phía con.

Cách hai: @JsonIgnore trên trường trỏ ngược — đơn giản hơn nếu bạn không cần chiều đó.

Cách ba, và là cách tôi khuyên: đừng serialize entity. Dùng DTO riêng cho API, và vấn đề biến mất.

Đừng trả entity JPA ra API

Đây là lỗi kiến trúc phổ biến nhất trong dự án Spring, và nó gây bốn vấn đề:

Vòng lặp tham chiếu như trên — quan hệ hai chiều là chuẩn trong JPA.

LazyInitializationException khi Jackson chạm vào quan hệ lazy sau khi giao dịch đã đóng. Bài 28 sẽ nói kỹ.

Truy vấn N+1 ngoài ý muốn — Jackson duyệt quan hệ và mỗi lần duyệt là một truy vấn. Bài 27.

Lộ dữ liệu. Thêm một cột vào entity là nó tự động xuất hiện trong API, kể cả khi đó là dữ liệu nội bộ.

DTO tốn thêm mã, nhưng nó tách hợp đồng API khỏi lược đồ CSDL — và hai thứ đó phải đổi độc lập được.

Tuỳ biến ObjectMapper

Đừng tạo mới, hãy chỉnh cái Spring dựng:

@Bean
Jackson2ObjectMapperBuilderCustomizer tuyBien() {
    return b -> b.serializationInclusion(JsonInclude.Include.NON_NULL)
                 .featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
}

Cách này giữ mọi cấu hình mặc định của Boot. Khai @Bean ObjectMapper của riêng bạn sẽ thay thế hoàn toàn — và bạn mất JavaTimeModule cùng nhiều thứ khác.

Với kiểu riêng, viết serializer:

class TienSerializer extends JsonSerializer<Tien> {
    public void serialize(Tien t, JsonGenerator g, SerializerProvider p) throws IOException {
        g.writeString(t.dinhDang());
    }
}

rồi gắn bằng @JsonSerialize(using = TienSerializer.class).

Nhúng JSON vào HTML

Nếu bạn render trang có nhúng JSON, nhớ bài 60 sê-ri Java: Jackson không thoát <>, và một chuỗi chứa </script> sẽ đóng sớm thẻ script.

Với API JSON trả qua HTTP thì không cần lo — trình duyệt không phân tích nó như HTML. Chỉ cần cẩn thận đúng ở chỗ chèn vào trang.

Thử ba mươi giây

curl -s localhost:8080/api/... | jq 'keys'

So danh sách khoá với DTO của bạn. Thấy trường bạn không định lộ ra — password, internalNote, version — nghĩa là bạn đang serialize entity chứ không phải DTO.

Ngày mai: Thymeleaf và nội dung tĩnh — khi nào render phía máy chủ vẫn đúng.