Tình Huống
Bạn đang gọi một REST API hoặc đọc file JSON. Jackson bắt đầu ánh xạ response vào DTO của bạn — rồi phát sinh lỗi:
com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "fieldName" (class com.example.MyDto), not marked as ignorable (4 known properties: ...)
at com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException.from(UnrecognizedPropertyException.java:61)
at com.fasterxml.jackson.databind.DeserializationContext.handleUnknownProperty(DeserializationContext.java:1001)
Nghe quen không? API vừa thêm field mới — ví dụ "createdAt" — DTO của bạn chưa có field đó, và Jackson từ chối tiếp tục. Hoặc bạn đổi tên userName thành displayName trong class Java nhưng JSON vẫn gửi key cũ. Dù trường hợp nào thì cũng cùng một lỗi.
Tại Sao Jackson Ném Lỗi Này
ObjectMapper của Jackson được thiết kế strict theo mặc định. Nếu JSON chứa property không có field tương ứng trong class đích, nó sẽ fail ngay lập tức thay vì âm thầm bỏ qua dữ liệu. Ý định khá hợp lý: phát hiện schema mismatch sớm, trước khi gây ra các lỗi tinh vi về sau.
Trên thực tế, lỗi này xuất hiện trong một số tình huống có thể đoán trước:
- API bên thứ ba thêm field mới và bạn chưa cập nhật DTO
- JSON đến từ phiên bản cũ hơn của ứng dụng với schema khác
- Bạn đang dùng chung một DTO giữa các service đã phân kỳ theo thời gian
- Tên field bị gõ sai ở đâu đó trong chuỗi xử lý
Cách Sửa Nhanh: Annotate Class DTO
Thêm @JsonIgnoreProperties(ignoreUnknown = true) vào class. Jackson sẽ bỏ qua mọi field JSON không thể ánh xạ — không exception, không mất dữ liệu trên các field bạn đã có:
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
@JsonIgnoreProperties(ignoreUnknown = true)
public class MyDto {
private String name;
private int age;
// getters and setters
}
Cách sửa này rất khoanh vùng. Chỉ riêng MyDto trở nên linh hoạt hơn — phần còn lại trong codebase vẫn strict. Đây là sự đánh đổi hợp lý khi bạn tin tưởng các DTO nội bộ của mình nhưng cần xử lý response khó đoán từ API bên ngoài.
Cách Sửa Toàn Cục: Cấu Hình ObjectMapper
Cần hành vi linh hoạt ở khắp nơi? Cấu hình ObjectMapper một lần và dùng instance đó xuyên suốt:
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.DeserializationFeature;
ObjectMapper mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
// Use this mapper for all deserialization
MyDto dto = mapper.readValue(jsonString, MyDto.class);
Điểm mấu chốt: cấu hình một lần, dùng chung instance. Tạo ObjectMapper mới cho mỗi lần gọi là lỗi phổ biến — tốn tài nguyên và dễ bỏ sót config flag.
Spring Boot: Cấu Hình ObjectMapper Toàn Cục
Spring Boot quản lý ObjectMapper riêng thông qua auto-configuration — bạn không cần (và không nên) tạo thủ công để deserialize trong controller. Thêm dòng này vào application.properties:
spring.jackson.deserialization.fail-on-unknown-properties=false
Hoặc trong application.yml:
spring:
jackson:
deserialization:
fail-on-unknown-properties: false
Spring Boot tự động nhận cấu hình này. Nó áp dụng cho ObjectMapper dùng chung bởi tất cả controller, các lệnh gọi RestTemplate, và response từ WebClient.
Nếu bạn đã định nghĩa bean ObjectMapper tùy chỉnh, hãy áp dụng flag tương tự ở đó — nếu không thì setting trong file properties sẽ không ảnh hưởng đến instance tùy chỉnh của bạn:
@Bean
public ObjectMapper objectMapper() {
return new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
}
Khi Bạn Thực Sự Muốn Lấy Field Không Xác Định
Bỏ qua các field không xác định không phải lúc nào cũng đúng. Đôi khi field "không nhận ra" đó chứa dữ liệu bạn cần.
Tùy chọn 1: Thêm field một cách tường minh. Nếu JSON gửi user_name nhưng field Java của bạn là userName, hãy giải quyết sự khác biệt tên bằng @JsonProperty:
import com.fasterxml.jackson.annotation.JsonProperty;
public class MyDto {
@JsonProperty("user_name")
private String userName;
}
Tùy chọn 2: Dùng Map để bắt tất cả. Khi schema thực sự động — ví dụ webhook payload hoặc plugin API — hãy thu thập các field chưa ánh xạ vào một Map với @JsonAnySetter:
import com.fasterxml.jackson.annotation.JsonAnySetter;
import java.util.HashMap;
import java.util.Map;
public class MyDto {
private String name;
private Map<String, Object> extra = new HashMap<>();
@JsonAnySetter
public void setExtra(String key, Object value) {
extra.put(key, value);
}
}
Bất kỳ field nào không được khai báo trong class đều đổ vào extra. Không có gì bị bỏ mất.
Kiểm Tra
Xác nhận cách sửa bằng một unit test tập trung trước khi coi là xong:
@Test
void shouldDeserializeWithExtraFields() throws Exception {
String json = "{\"name\": \"Alice\", \"age\": 30, \"unknownField\": \"ignored\"}";
ObjectMapper mapper = new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
MyDto dto = mapper.readValue(json, MyDto.class);
assertEquals("Alice", dto.getName());
assertEquals(30, dto.getAge());
// No exception — fix confirmed
}
Vẫn gặp exception sau khi sửa? Nguyên nhân phổ biến nhất là có một ObjectMapper thứ hai ở đâu đó trong chuỗi gọi — instance được tạo ra mà không có config flag. Tìm kiếm trong codebase từ khóa new ObjectMapper() và kiểm tra từng chỗ.
Mẹo
Gỡ lỗi deserialization dễ hơn nhiều khi bạn có thể đọc được JSON. Nếu response bị minify — một đống ký tự không có khoảng trắng — hãy dán vào JSON Formatter & Validator của ToolCraft để mở rộng ngay lập tức. Phân biệt "user_name" với "userName" trong 800 byte JSON minified rất khổ; tìm trong cây có định dạng chỉ mất hai giây.
Cũng đáng suy nghĩ thêm: ưu tiên dùng annotation theo từng class thay vì cấu hình toàn cục khi có thể lựa chọn. @JsonIgnoreProperties(ignoreUnknown = true) trên một DTO cụ thể là tường minh — người đọc sau biết rằng class đó được cố ý làm linh hoạt hơn. Tắt kiểm tra ở cấp toàn cục tiện lợi nhưng có thể che giấu schema drift thực sự. Hãy để dành flag toàn cục cho những trường hợp bạn thực sự không kiểm soát được hình dạng JSON đầu vào — API bên thứ ba, tích hợp hệ thống cũ, và những thứ tương tự.

