JSON 的表示在資料傳送時很方便,不過在程式碼裡就不見得了,尤其像是 Java 這樣的強型別語言,要操作 JSON 常常得寫上一堆麻煩的程式碼。這也就是為什麼會有許多 JSON parser 的開源專案的原因~吧。以前我在做這些事情時都是用 GSON 來處理的,不過由於 GSON 到現在似乎還是不支援 setter、getter 形式的轉換,能夠操弄的手法比較少,因此就來做做 Jackson 的簡易實驗。
Jackson 的基本使用
public class Product { public String name; public String description; }
首先先只看一個簡單的物件 Product,裡面有兩個字串 name 和 description:
ObjectMapper mapper = new ObjectMapper(); // Build a JSON object through GSON. JsonObject json = new JsonObject(); json.addProperty("name", "iPhone"); json.addProperty("description", "expensive mobile phone"); // Deserialize the JSON using ObjectMapper. Product product = mapper.readValue(json.toString(), Product.class); LOGGER.trace("product: {}, {}", product.name, product.description);
這邊先用 GSON 組成一個 JSON 物件,然後把組出來的 JSON 餵給 Jackson 去做反序列化。最後印出來的結果如下:
product: iPhone, expensive mobile phone
透過 field、getter 或者 setter 進行 JSON 序列化或反序列化
在 Jackson 預設的狀況下,一個 field 會被序列化或反序列化有幾個條件,必須要滿足任一條件才行:
- field 是以 public 被宣告
- field 有對應的 getter
- field 有對應的 setter(只有在反序列化時會生效)
自定義序列化或反序列化的行為
接著要嘗試一點稍微不同的東西,如果我有一個 Instant 欄位想要讓 Jackson 幫我處理,但轉換的方式我希望是用 epoch time 來表示,那麼這時就會需要客製化一個 JsonSerializer 或者 JsonDeserializer 了。這裡實際的作法跟 GSON 非常近似(而且 JsonSerializer/JsonDeserializer 名字也都取一樣 XD,雖然一邊是介面一邊是類別)。略有不同的地方在於,Jackson 是把宣告放在 Bean 上,而不像 GSON 是註冊在 GsonBuilder 裡面,這樣實際上也能帶來比較大的靈活度。
public class Product { public String name; public String description; @JsonDeserialize(using = InstantDeserialiser.class) public Instant lastModifiedTime; public Information info; }
這邊可以看到,在 Product 上我增加了一個 lastModifiedTime 這個屬性,型別是 Instant。同時我在 lastModifiedTime 的宣告上加上了 annotation @JsonDeserialize,這裡是代表當 Jackson 要反序列化時,請它呼叫我告訴它的類別來進行。因此這裡我也需要實作一個這裡宣告的 JsonDeserializer。
public class InstantDeserialiser extends JsonDeserializer<Instant> { @Override public Instant deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException { return Instant.ofEpochMilli(p.getLongValue()); } }
當反序列化的事件發生時,Jackson 會呼叫我定義的這個 JsonDeserializer,而這裡會做的事情就是取出 JSON 的 long 值,然後轉換成 Instant 物件後回傳。實際測試在 JSON 裡面追加了 lastModifiedTime 這個屬性後:
JsonObject json = new JsonObject(); json.addProperty("name", "iPhone"); json.addProperty("description", "expensive mobile phone"); json.addProperty("lastModifiedTime", 123); Product product = mapper.readValue(json.toString(), Product.class); LOGGER.trace("product: {}, {}, {}", product.name, product.description, product.lastModifiedTime);
就會得到這樣的結果:
product: iPhone, expensive mobile phone, 1970-01-01T00:00:00.123Z
特殊應用:透過 setter 調整 JSON 的結構
其實到這段這次才是真正想要做的實驗。因為資料從資料庫拿出來時,格式往往跟最後要輸出的結構不太一樣,因此想要有個簡易一點的方法處理這個問題。GSON 雖然有 @SerializedName 可以做一點小技巧,但好像還是很難在 GSON 做到這種行為,因此才會考慮改用 Jackson 來嘗試。
首先一樣先擴充一下上面的範例 Product,主要是追加了巢狀結構,也就是有 info 這個下一階層的屬性:
public class Product { public String name; public String description; public Instant lastModifiedTime; public Information info; public void setName(String name) { this.name = name; } public void setDescription(String description) { this.description = description; } @JsonDeserialize(using=InstantDeserialiser.class) public void setLastModifiedTime(Instant lastModifiedTime) { this.lastModifiedTime = lastModifiedTime; } public void setPrice(BigDecimal price) { if (info == null) { info = new Information(); } info.price = price; } public void setOwner(String owner) { if (info == null) { info = new Information(); } info.owner = owner; } } public class Information { public BigDecimal price; public String owner; }
接著就是測試的主程式:
ObjectMapper mapper = new ObjectMapper() .configure(SerializationFeature.INDENT_OUTPUT, true); JsonObject json = new JsonObject(); json.addProperty("name", "iPhone"); json.addProperty("description", "expensive mobile phone"); json.addProperty("lastModifiedTime", 123); json.addProperty("owner", "User1"); json.addProperty("price", 312.57); Product product = mapper.readValue(json.toString(), Product.class); LOGGER.trace("product: {}", mapper.writeValueAsString(product));
可以從組成的 JsonObject 那邊看出來,JSON 的格式是所有欄位都在同一層,但是 Product 物件的宣告卻是要把 owner 和 price 放在下一階層。而這樣的執行結果如下:
product: { "name" : "iPhone", "description" : "expensive mobile phone", "lastModifiedTime" : { "nano" : 123000000, "epochSecond" : 0 }, "info" : { "price" : 312.57, "owner" : "User1" } }
從結果來說確實達到了目標,讓 price 和 owner 兩個欄位都輸出在 info 底下變成 inner JSON。此外這裡其實還可以注意到 Instant 的輸出變回 Jackson 的預設方法了,不過這其實只是因為我沒有為 Instant 定義客製化的 Serilizer 才產生的結果。
沒有留言:
張貼留言