2019年8月25日 星期日

利用 Jackson 序列化、反序列化 JSON

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 才產生的結果。

    參考資料
    1. Mapping Nested Values with Jackson

    沒有留言: