使用 Jackson 進行帶有多個參數建構函數的 JSON 反序列化
1. 引言
在本教程中,我們將學習如何使用 Jackson 的多參數建構函數將 JSON 反序列化為 Java 物件。
預設情況下,Jackson 需要一個不接受任何參數的預設建構子。字段值透過 setter 方法或反射來設定。如果希望 Jackson 使用非預設建構函數,則需要使用@JsonCreator註解來標記該建構函數。此註解可以應用於記錄和枚舉的建構函數和靜態工廠方法。在本教程中,我們將逐一介紹這些情況。
我們也將研究 Jackson 提供的減少反序列化所需註解數量的選項。
2. 設定
2.1. Maven 依賴項
除了基本的Jackson 依賴項之外,我們還需要Jackson 的參數名稱模組:
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.17.2</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-parameter-names</artifactId>
<version>2.17.2</version>
<dependency>
2.2. Java 類
在本教程中,我們將使用Ticket類別:
public class Ticket {
@JsonProperty("event")
private String eventName;
private String guest;
private final Currency currency;
private final int price;
public Ticket() {
this.price = 0;
this.currency = Currency.EUR;
}
public void setGuest(String guest) {
this.guest = guest;
}
// getters for all attributes
}
請注意,我們僅為訪客屬性定義了 setter 方法,而為所有屬性提供了 getter 方法。這裡,我們特意省略了其他三個屬性的 setter 方法,以演示反序列化的行為。此外,我們在eventName屬性上使用@JsonProperty ,以示範 Jackson 提供的不同的欄位序列化方式。
以下是貨幣列舉:
public enum Currency {
EUR("Euro", "cent"),
GBP("Pound sterling", "penny"),
CHF("Swiss franc", "Rappen");
private String fullName;
private String fractionalUnit;
Currency(String fullName, String fractionalUnit) {
this.fullName = fullName;
this.fractionalUnit = fractionalUnit;
}
}
2.3. 反序列化 JSON
以下是範例中我們需要反序列化的 JSON:
{
"event": "Devoxx",
"guest": "Maria Monroe",
"currency": "EUR",
"price": 50
}
3. 預設反序列化
我們需要定義一個預設的無參建構函數,因為該類別具有 final 屬性。如果我們只定義一個以貨幣和價格為參數的建構函數,Jackson 將無法反序列化該物件:
public Ticket(Currency currency, int price) {
this.price = price;
this.currency = currency;
}
傑克森會拋出異常:
com.fasterxml.jackson.databind.exc.MismatchedInputException:
Cannot construct instance of `com.baeldung.jackson.multiparameterconstructor.Ticket`
(no Creators, like default constructor, exist): cannot deserialize from Object value
4. 使用@JsonCreator進行反序列化
為了指示應該使用哪個建構函數來反序列化,我們可以使用@JsonCreator來註解。
4.1. 使用@JsonCreator和@JsonProperty定義建構函數
如果要使用雙參數建構函數,我們需要用@JsonCreator來註解它,並用@JsonProperty來註解參數:
@JsonCreator
public Ticket(@JsonProperty("currency") Currency currency, @JsonProperty("price") int price) {
this.price = price;
this.currency = currency;
}
Jackson 將如下反序列化 JSON:
-
currency和price,這兩個屬性是在建構函數中設定的。 -
guest屬性是透過其 setter 方法設定的。 -
eventName屬性沒有 setter 方法,而是透過反射來設定。
如果某個屬性沒有 setter 方法,Jackson 會根據屬性名稱透過反射來設定屬性。如果 Java 屬性名稱與 JSON 欄位名稱不同,我們可以使用@JsonProperty註解來定義 JSON 欄位名稱。在我們的範例中,我們使用@JsonProperty(“event”) ` 註解eventName屬性,以表示 JSON 欄位名稱為 `event` ,而 Java 屬性名稱為eventName 。
我們只能使用@JsonCreator來註解一個建構函數。如果我們除了已經定義的建構函數之外,還註解第二個建構子:
@JsonCreator
public Ticket(@JsonProperty("currency") Currency currency, @JsonProperty("price") int price, @JsonProperty("guest") String guest) {
this.price = price;
this.currency = currency;
this.guest = guest;
}
傑克森會拋出異常:
com.fasterxml.jackson.databind.JsonMappingException:
com.fasterxml.jackson.databind.exc.InvalidDefinitionException:
Conflicting property-based creators:
already had explicitly marked creator [constructor for `com.baeldung.jackson.multiparameterconstructor.Ticket` (2 args),
annotations: {interface com.fasterxml.jackson.annotation.JsonCreator=@com.fasterxml.jackson.annotation.JsonCreator(mode=DEFAULT)},
encountered another: [constructor for `com.baeldung.jackson.multiparameterconstructor.Ticket` (3 args),
annotations: {interface com.fasterxml.jackson.annotation.JsonCreator=@com.fasterxml.jackson.annotation.JsonCreator(mode=DEFAULT)}
4.2. 不使用@JsonProperty構造函數
Jackson 使用反射將 JSON 欄位名稱對應到 Java 類別屬性。這對於類別屬性有效,但對於方法參數名稱無效。因此,如果我們定義一個沒有@JsonProperty註解的建構子:
@JsonCreator
public Ticket(Currency currency, int price, String guest) {
this.price = price;
this.currency = currency;
this.guest = guest;
}
我們收到一個異常:
com.fasterxml.jackson.databind.exc.InvalidDefinitionException:
Invalid type definition for type `com.baeldung.jackson.multiparameterconstructor.Ticket`:
Argument #0 of constructor [constructor for `com.baeldung.jackson.multiparameterconstructor.Ticket` (2 args),
annotations: {interface com.fasterxml.jackson.annotation.JsonCreator=@com.fasterxml.jackson.annotation.JsonCreator(mode=DEFAULT)}
has no property name (and is not Injectable): can not use as property-based Creator
Java 在執行時間不會保留方法參數名稱,因此我們需要使用@JsonProperty註解參數,以指定應對應到每個參數的 JSON 欄位名稱。
如果我們想要避免使用@JsonProperty註解參數,我們可以註冊ParameterNamesModul並將parameters標誌加入編譯器。
首先,我們需要新增Maven 依賴項:
<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-parameter-names</artifactId>
<version>2.21.1</version>
</dependency>
然後,我們需要將ParameterNamesModule註冊到物件映射器中:
ObjectMapper mapper = JsonMapper.builder()
.constructorDetector(ConstructorDetector.USE_PROPERTIES_BASED)
.addModule(new ParameterNamesModule(JsonCreator.Mode.PROPERTIES))
.build();
並將parameters標誌加入編譯器:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<compilerArgs>
<arg>-parameters</arg>
</compilerArgs>
</configuration>
</plugin>
4.3. 使用ConstructorDetector進行配置
我們還需要加入@JsonCreator註解來標記我們要用於反序列化的建構子。
從 Jackson 2.12 開始,我們可以註冊一個 ConstructorDetector 來指定用於反序列化的建構函數,而無需使用@JsonCreator:
ObjectMapper mapper = JsonMapper.builder()
.constructorDetector(ConstructorDetector.USE_PROPERTIES_BASED)
.addModule(new ParameterNamesModule(JsonCreator.Mode.PROPERTIES))
.constructorDetector(ConstructorDetector.USE_PROPERTIES_BASED)
.build();
值得注意的是,如果我們配置 Jackson 來偵測建構函數,並使用@JsonCreator註解多個建構函數中的一個,那麼帶有註解的建構函數將優先於其他方式偵測到的構造函數。
5. 記錄
Jackson 可以使用預設的規範建構函式反序列化記錄。我們來看以下記錄:
public record Guest(@JsonProperty("firstname") String firstname, @JsonProperty("surname") String surname) {}
以及以下JSON:
{
"firstname": "Maria",
"surname": "Monroe"
}
Jackson 可以將 JSON 反序列化為 Java 記錄,而無需使用無參構造函數。如果我們註冊了ParameterNamesModule並使用parameters標誌進行編譯,則無需使用@JsonProperty註解記錄元件:
public record Guest(String firstname, String surname) {}
在某些情況下,我們可能需要自訂規範建構函數:
public Guest(String firstname, String surname) {
this.firstname = firstname;
this.surname = surname;
// some validation
}
同樣,我們不需要用@JsonCreator註解建構函數,因為 Jackson 預設會使用規範建構函數。
當我們想要加入靜態工廠方法時,就需要用到@JsonCreator註解:
@JsonCreator
public static Guest fromJson(String firstname, String surname) {
// some validation
return new Guest(firstname, surname);
}
與使用建構函式不同的是,靜態工廠方法可以有額外的參數:
@JsonCreator
public static Guest fromJson(String firstname, String surname, int id) {
// some validation
return new Guest(firstname, surname);
}
即使使用@JsonCreator註解,非規範建構函數也不會被使用:
@JsonCreator
public Guest(String firstname, String surname, int id) {
this(firstname, surname)
// some validation
}
Jackson 將忽略此建構函數,而改用規範建構函數。
6. 枚舉
@JsonCreator也可用於反序列化枚舉類型。預設情況下,Jackson 使用枚舉名稱進行反序列化。
我們可以透過為枚舉添加@JsonFormat註解來改變預設行為:
@JsonFormat(shape = JsonFormat.Shape.OBJECT)
public enum Currency {
EUR("Euro", "cent"),
GBP("Pound sterling", "penny"),
CHF("Swiss franc", "Rappen");
private String fullName;
private String fractionalUnit;
Currency(String fullName, String fractionalUnit) {
this.fullName = fullName;
this.fractionalUnit = fractionalUnit;
}
// getters for all attributes
}
枚舉值EUR將被序列化為以下 JSON:
{
"fullName": "Euro",
"fractionalUnit": "cent"
}
但是,反序列化將失敗,並拋出以下異常:
com.fasterxml.jackson.databind.exc.MismatchedInputException:
Cannot deserialize value of type `com.baeldung.jackson.multiparameterconstructor.Currency`
from Object value (token `JsonToken.START_OBJECT`)
這是因為 Jackson 嘗試根據枚舉名稱EUR來反序列化枚舉。我們可能會認為使用@JsonCreator註解建構函數可以解決這個問題:
@JsonCreator
Currency(String fullName, String fractionalUnit) {
this.fullName = fullName;
this.fractionalUnit = fractionalUnit;
}
這樣做行不通,因為枚舉建構函式是私有的,Jackson 無法使用。解決方法是定義一個
靜態工廠方法,並使用@JsonCreator註解:
@JsonCreator
public static Currency fromJsonString(String fullName, String fractionalUnit) {
for (Currency c : Currency.values()) {
if (c.fullName.equalsIgnoreCase(fullName) && c.fractionalUnit.equalsIgnoreCase(fractionalUnit)) {
return c;
}
}
throw new IllegalArgumentException("Unknown currency: " + fullName + " " + franctionalUnit);
}
7. 結論
本文介紹如何使用多參數建構函式將 JSON 反序列化為 Java 物件。我們了解如何在 Java 類別、枚舉和記錄中使用@JsonCreator註解。此外,我們也了解到參數名稱模組和建構函式偵測器設定有助於減少所需的註解數量。
與往常一樣,本文中的程式碼可在 GitHub 上找到。