1. Giới thiệu về Jackson
Trao đổi dữ liệu giữa ứng dụng Java và thế giới bên ngoài rất thường được đặt thành bài toán: “Làm thế nào để biến một đối tượng Java thành JSON — và ngược lại?” Tất nhiên bạn có thể tự viết parser bằng String.split và regex (và có khi còn “thấy thú vị” trong nỗi khổ), nhưng trong dự án thực tế thì không ai làm vậy.
Trong Java có vài thư viện phổ biến để làm việc với JSON. Quan trọng nhất là Jackson. Nó phổ biến đến mức nằm trong Spring Boot và được dùng trong rất nhiều framework và thư viện khác.
Jackson là gì?
Jackson là một thư viện mạnh mẽ và linh hoạt để tuần tự hóa (chuyển đối tượng Java thành JSON) và giải tuần tự (chiều ngược lại). Nó gồm vài module, nhưng cho 90% nhu cầu bạn sẽ cần hai cái:
- jackson-core — lõi, parser cấp thấp.
- jackson-databind — module cấp cao, có thể biến đối tượng Java thành JSON và ngược lại.
Điều bạn cần biết ban đầu: nếu bạn thấy lớp ObjectMapper — đó là Jackson.
Jackson được coi là tiêu chuẩn de facto cho công việc với JSON trong Java vì nó kết hợp sự đơn giản và sức mạnh. Để tuần tự hóa hay giải tuần tự dữ liệu, bạn chỉ cần vài dòng mã — điều này khiến thư viện tiện lợi ngay cả với người mới. Đồng thời nó không chỉ dừng ở khả năng cơ bản: nhờ các annotation và vô số thiết lập, bạn có thể điều khiển linh hoạt cách dữ liệu được biến thành đối tượng và ngược lại, dù là collection, thực thể lồng nhau hay ngày giờ ở các định dạng khác nhau.
Không kém phần quan trọng, Jackson hoạt động rất nhanh và tiết kiệm, điều tối quan trọng cho dự án thực tế với lượng dữ liệu lớn. Các nhà phát triển thư viện hỗ trợ các phiên bản Java mới và những thay đổi hiện hành của chuẩn JSON — Jackson vẫn là lựa chọn đáng tin cậy cho cả ứng dụng đơn giản lẫn hệ thống doanh nghiệp lớn.
2. Đọc JSON (giải tuần tự)
Hãy thử đọc một chuỗi JSON và biến nó thành đối tượng Java. Để làm vậy, chúng ta cần:
- Lớp dữ liệu (ví dụ, Person)
- Lớp ObjectMapper của Jackson
Tích hợp Jackson
Nếu dùng Maven, hãy thêm vào pom.xml:
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.0</version>
</dependency>
Nếu dùng Gradle — tương tự:
implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.0'
Ví dụ về lớp
public class Person {
public String name;
public int age;
}
Lưu ý: Để đơn giản, các trường được để public. Sau này ta sẽ bàn về làm việc với trường private và getter/setter.
Ví dụ JSON
{
"name": "Alice",
"age": 30
}
Giải tuần tự: biến JSON thành đối tượng
import com.fasterxml.jackson.databind.ObjectMapper;
public class Main {
public static void main(String[] args) throws Exception {
String json = "{\"name\": \"Alice\", \"age\": 30}";
ObjectMapper mapper = new ObjectMapper();
Person person = mapper.readValue(json, Person.class);
System.out.println(person.name); // Alice
System.out.println(person.age); // 30
}
}
Ở đây Jackson phân tích JSON, tìm các trường trùng tên với trường hoặc getter của lớp và điền đối tượng với các giá trị tương ứng bằng readValue.
Giải tuần tự danh sách đối tượng
Giả sử ta có một mảng:
[
{ "name": "Bob", "age": 22 },
{ "name": "Eve", "age": 27 }
]
Giải tuần tự thành danh sách:
import com.fasterxml.jackson.core.type.TypeReference;
// ...
String json = "[{\"name\": \"Bob\", \"age\": 22}, {\"name\": \"Eve\", \"age\": 27}]";
ObjectMapper mapper = new ObjectMapper();
List<Person> people = mapper.readValue(json, new TypeReference<List<Person>>() {});
for (Person p : people) {
System.out.println(p.name + " (" + p.age + ")");
}
Có thể bạn sẽ thắc mắc: tại sao không đơn giản viết mapper.readValue(json, List.class)? Hãy nhớ rằng generics trong Java bị xóa khi biên dịch. Vì thế ta cần TypeReference để Jackson hiểu rằng bên trong danh sách là các đối tượng kiểu Person.
3. Ghi JSON (tuần tự hóa)
Bây giờ hãy làm ngược lại: biến đối tượng Java thành chuỗi JSON.
Ví dụ
ObjectMapper mapper = new ObjectMapper();
Person person = new Person();
person.name = "Charlie";
person.age = 40;
String json = mapper.writeValueAsString(person);
System.out.println(json);
// {"name":"Charlie","age":40}
Tuần tự hóa danh sách đối tượng
ObjectMapper mapper = new ObjectMapper();
List<Person> people = new ArrayList<>();
people.add(new Person("Anna", 25));
people.add(new Person("Dmitry", 31));
String json = mapper.writeValueAsString(people);
System.out.println(json);
// [{"name":"Anna","age":25},{"name":"Dmitry","age":31}]
Ghi ra tệp
ObjectMapper mapper = new ObjectMapper();
Person person = new Person();
person.name = "Charlie";
person.age = 40;
mapper.writeValue(new File("person.json"), person);
// Tệp person.json giờ chứa đối tượng JSON
JSON “đẹp” (pretty)
Mặc định Jackson ghi tất cả trên một dòng. Nhưng có một cách “dễ đọc cho con người” hơn — pretty printing. Tức là chuỗi JSON sẽ được in ở dạng dễ đọc: có thụt đầu dòng, xuống dòng và định dạng gọn gàng.
Khác với JSON “thông thường”, thường được ghi trên một dòng để tiết kiệm, phiên bản “đẹp” là dành cho con người — để dễ xem cấu trúc dữ liệu trong log, tệp hoặc trên màn hình.
ObjectMapper mapper = new ObjectMapper();
Person person = new Person();
person.name = "Charlie";
person.age = 40;
String prettyJson = mapper.writerWithDefaultPrettyPrinter()
.writeValueAsString(person);
System.out.println(prettyJson);
/*
{
"name" : "Charlie",
"age" : 40
}
*/
4. Các chú thích (annotations) của Jackson
Jackson hỗ trợ nhiều annotation cho phép điều khiển quá trình tuần tự hóa và giải tuần tự. Dưới đây là những cái hữu ích nhất:
@JsonProperty
Cho phép chỉ định tên trường trong JSON nếu nó khác với tên trường trong lớp.
import com.fasterxml.jackson.annotation.JsonProperty;
public class Person {
@JsonProperty("full_name")
public String name;
public int age;
}
{"full_name": "Olga", "age": 28}
Jackson sẽ hiểu rằng trường full_name từ JSON cần ghi vào trường name của đối tượng.
@JsonIgnore
Nếu bạn không muốn tuần tự hóa hoặc giải tuần tự một trường nào đó:
import com.fasterxml.jackson.annotation.JsonIgnore;
public class Person {
public String name;
@JsonIgnore
public int age;
}
Trong JSON sẽ không có trường age, ngay cả khi nó tồn tại trong đối tượng.
@JsonInclude
Kiểm soát những trường nào sẽ được đưa vào JSON. Ví dụ, chỉ tuần tự hóa các trường không rỗng:
import com.fasterxml.jackson.annotation.JsonInclude;
@JsonInclude(JsonInclude.Include.NON_NULL)
public class Person {
public String name;
public Integer age;
}
Nếu age == null, JSON sẽ không có khóa "age".
@JsonFormat
Cho phép đặt định dạng khi tuần tự hóa ngày và thời gian.
import com.fasterxml.jackson.annotation.JsonFormat;
import java.util.Date;
public class Event {
public String title;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss")
public Date date;
}
Event event = new Event();
event.title = "Hackathon";
event.date = new Date();
ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(event);
// {"title":"Hackathon","date":"2024-06-07 15:23:00"}
Ví dụ: kết hợp tất cả
import com.fasterxml.jackson.annotation.*;
@JsonInclude(JsonInclude.Include.NON_NULL)
public class Person {
@JsonProperty("full_name")
private String name;
private int age;
@JsonIgnore
private String password;
// Getter và setter là bắt buộc cho các trường private!
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
}
5. Thực hành: tuần tự hóa và giải tuần tự có dùng annotations
Hãy mở rộng ứng dụng học tập của bạn — giờ chúng ta có lớp User với các trường private, ngày đăng ký và mật khẩu không được xuất hiện trong JSON.
import com.fasterxml.jackson.annotation.*;
import java.util.Date;
@JsonInclude(JsonInclude.Include.NON_NULL)
public class User {
@JsonProperty("login")
private String username;
private int age;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd")
private Date registered;
@JsonIgnore
private String password;
// Getter và setter là bắt buộc!
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
public Date getRegistered() { return registered; }
public void setRegistered(Date registered) { this.registered = registered; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
}
Tuần tự hóa
import com.fasterxml.jackson.databind.ObjectMapper;
import java.text.SimpleDateFormat;
ObjectMapper mapper = new ObjectMapper();
User user = new User();
user.setUsername("superuser");
user.setAge(42);
user.setRegistered(new SimpleDateFormat("yyyy-MM-dd").parse("2024-06-07"));
user.setPassword("qwerty123"); // Sẽ không có trong JSON!
String json = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(user);
System.out.println(json);
/*
{
"login" : "superuser",
"age" : 42,
"registered" : "2024-06-07"
}
*/
Giải tuần tự
import com.fasterxml.jackson.databind.ObjectMapper;
ObjectMapper mapper = new ObjectMapper();
String json = "{ \"login\": \"superuser\", \"age\": 42, \"registered\": \"2024-06-07\" }";
User user = mapper.readValue(json, User.class);
System.out.println(user.getUsername()); // superuser
System.out.println(user.getAge()); // 42
System.out.println(user.getRegistered());// Fri Jun 07 00:00:00 ...
System.out.println(user.getPassword()); // null (và đó là điều tốt!)
6. Các lỗi thường gặp khi làm việc với Jackson
Lỗi số 1: Thiếu constructor không tham số.
Jackson sẽ không thể tạo đối tượng lớp nếu lớp không có constructor không tham số. Điều này thường gặp khi lớp chỉ khai báo constructor có tham số.
Lỗi số 2: Trường private nhưng không có getter/setter.
Nếu bạn đặt tất cả trường là private nhưng quên thêm getter và setter, Jackson sẽ không thể điền chúng khi giải tuần tự (theo mặc định).
Lỗi số 3: Tên trường không khớp.
Khi tên trường trong JSON khác với tên trường/getter trong Java, Jackson sẽ không tìm thấy đối sánh. Hãy dùng @JsonProperty.
Lỗi số 4: Sai định dạng ngày.
Nếu định dạng ngày trong JSON không trùng định dạng mà Java mong đợi, Jackson sẽ báo lỗi phân tích. Hãy dùng @JsonFormat để cấu hình.
Lỗi số 5: Cố gắng tuần tự hóa các trường có chú thích @JsonIgnore.
Các trường như vậy sẽ không xuất hiện trong JSON — đó là tính năng chứ không phải lỗi.
Lỗi số 6: Tuần tự hóa/giải tuần tự collection mà không chỉ định kiểu phần tử.
Nếu không dùng TypeReference, Jackson sẽ không hiểu kiểu đối tượng bên trong collection.
Lỗi số 7: Ngoại lệ khi đọc/ghi tệp.
Đừng quên xử lý IOException khi làm việc với tệp.
GO TO FULL VERSION