CodeGym /課程 /JAVA 25 SELF /Jackson — 讀取與寫入 JSON、註解

Jackson — 讀取與寫入 JSON、註解

JAVA 25 SELF
等級 46 , 課堂 1
開放

1. Jackson 簡介

在 Java 應用程式與外部世界之間的資料交換,常可歸結為這個問題:「如何將 Java 物件轉換為 JSON——再轉回來?」當然,你可以自己用 String.split 和正則表達式寫一個剖析器(甚至從苦痛中獲得樂趣),但在真實專案中沒有人這麼做。

在 Java 中有數個常見的 JSON 函式庫。其中最主要的是 Jackson。它受歡迎到被納入 Spring Boot,並被許多其他框架與函式庫採用。

什麼是 Jackson?

Jackson 是一個功能強大且彈性的函式庫,用於序列化(將 Java 物件轉為 JSON)與反序列化(相反的操作)。它由多個模組組成,但在 90% 的情境下你只需要兩個:

  • jackson-core — 核心,低階剖析器。
  • jackson-databind — 高階模組,能把 Java 物件轉為 JSON,反之亦然。

你只要記住:如果你看到類別 ObjectMapper ——那就是 Jackson。

Jackson 被視為在 Java 中處理 JSON 的事實標準,因為它兼具簡潔與強大。為了序列化或反序列化資料,往往只需要幾行程式碼,對新手也相當友善。同時它也不僅止於基本功能:透過註解與眾多設定,你可以靈活地控管資料如何轉換為物件與反向處理,無論是集合、巢狀實體,或是各種格式的日期。

同樣重要的是,Jackson 的效能很快且資源使用節省,這對於大量資料的真實專案至關重要。函式庫的開發者持續支援新的 Java 版本與 JSON 標準的最新變動——Jackson 對於從簡單應用到大型企業系統,都是可靠的選擇。

2. 讀取 JSON(反序列化)

讓我們試著讀取一段 JSON 字串並把它轉成 Java 物件。為此需要:

  • 資料類別(例如,Person
  • 來自 Jackson 的 ObjectMapper 類別

引入 Jackson

若使用 Maven,請在 pom.xml 中加入:

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.17.0</version>
</dependency>

若使用 Gradle — 類似:

implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.0'

類別範例

public class Person {
    public String name;
    public int age;
}

注意:為求簡單,欄位設為 public。稍後我們會討論如何處理 private 欄位與 getter/setter。

JSON 範例

{
  "name": "Alice",
  "age": 30
}

反序列化:把 JSON 轉成物件

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
    }
}

在此,Jackson 會剖析 JSON 字串,尋找與類別中的欄位或 getter 同名的鍵,並透過 readValue 將對應的值填入物件。

反序列化物件清單

假設我們有一個陣列:

[
  { "name": "Bob", "age": 22 },
  { "name": "Eve", "age": 27 }
]

將其反序列化為 List:

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 + ")");
}

你也許會問:為什麼不能直接寫 mapper.readValue(json, List.class)? 記得我們剛提到 Java 的泛型在編譯時會被擦除嗎?因此我們需要 TypeReference,讓 Jackson 理解清單中的元素型別是 Person

3. 寫入 JSON(序列化)

現在我們來做相反的操作:把 Java 物件轉成 JSON 字串。

範例

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}

序列化物件清單

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}]

寫入檔案

ObjectMapper mapper = new ObjectMapper();

Person person = new Person();
person.name = "Charlie";
person.age = 40;

mapper.writeValue(new File("person.json"), person);
// 檔案 person.json 現在包含 JSON 物件

美觀的(pretty)JSON

預設情況下,Jackson 會把內容寫成一行。不過也有更「以人為本」的寫法——pretty printing。也就是以可讀性佳的方式輸出 JSON 字串:帶縮排、換行與整齊的格式。

和為了緊湊而多半寫成單行的「一般」JSON 不同,「美觀」版本是給人看的——便於在日誌、檔案或畫面上輕鬆檢視資料結構。

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. Jackson 註解

Jackson 支援多種註解,能用來控制序列化與反序列化流程。以下是最實用的幾個:

@JsonProperty

當 JSON 中的欄位名稱與類別中的欄位名稱不同時,允許你指定 JSON 的欄位名稱。

import com.fasterxml.jackson.annotation.JsonProperty;

public class Person {
    @JsonProperty("full_name")
    public String name;

    public int age;
}
{"full_name": "Olga", "age": 28}

Jackson 會理解,JSON 中的 full_name 欄位要寫入物件的 name 欄位。

@JsonIgnore

如果你不想序列化或反序列化某個欄位:

import com.fasterxml.jackson.annotation.JsonIgnore;

public class Person {
    public String name;

    @JsonIgnore
    public int age;
}

即使物件中有該欄位,JSON 中也不會出現 age

@JsonInclude

控制哪些欄位會進入 JSON。例如,只序列化非空欄位:

import com.fasterxml.jackson.annotation.JsonInclude;

@JsonInclude(JsonInclude.Include.NON_NULL)
public class Person {
    public String name;
    public Integer age;
}

如果 age == null,JSON 中不會有 "age" 這個鍵。

@JsonFormat

允許為日期與時間指定序列化格式。

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"}

範例:整合

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;

    // private 欄位必須提供 getter 與 setter!
    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. 實作練習:考量註解的序列化與反序列化

讓我們擴充你的學習用應用程式——現在有一個 User 類別,包含 private 欄位、註冊日期,以及不應該出現在 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 與 setter!
    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; }
}

序列化

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"); // 不會出現在 JSON 中!

String json = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(user);

System.out.println(json);
/*
{
  "login" : "superuser",
  "age" : 42,
  "registered" : "2024-06-07"
}
*/

反序列化

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(這很好!)

6. 使用 Jackson 的常見錯誤

錯誤 1:缺少無參數建構子。
若類別沒有無參數建構子,Jackson 將無法建立該類別的物件。這常見於只明確宣告了帶參數建構子的類別。

錯誤 2:private 欄位沒有 getter/setter。
如果你把所有欄位都設為 private,卻忘了提供 getter 與 setter,Jackson 在預設情況下無法於反序列化時填入它們。

錯誤 3:欄位名稱不一致。
當 JSON 中的欄位名與 Java 的欄位或 getter 名稱不同,Jackson 找不到對應。請使用 @JsonProperty

錯誤 4:日期格式不正確。
如果 JSON 中的日期格式與 Java 端預期的格式不一致,Jackson 會拋出剖析錯誤。請使用 @JsonFormat 進行設定。

錯誤 5:嘗試序列化帶有 @JsonIgnore 註解的欄位。
這些欄位不會出現在 JSON 中——這不是 bug,而是設計。

錯誤 6:在集合的序列化/反序列化中未指定元素型別。
如果不使用 TypeReference,Jackson 不會知道集合中的物件型別。

錯誤 7:讀寫檔案時發生例外。
處理檔案時,別忘了處理 IOException

留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION