2016年9月10日 星期六

Gson 基礎使用:序列化與反序列化時,使用不同的名稱

在利用 Gson 做自動序列化與反序列化時,有時會遇到有點特殊的需求
就是序列化與反序列化時,想要使用不同的名稱。
實務上比較直覺的狀況是,某個物件是要從資料庫取出的,然後要將物件內容透過 JSON 格式輸出給使用者
但是又不希望使用者可以直接從 JSON 格式看見資料庫的結構,因此會嘗試至少把名字換掉 XD
(雖然這個例子看起來好像只是鴕鳥心態就是了 XD)

    Gson 命名規則
    預設命名規則:FieldNamingPolicy.IDENTITY

    在討論使用不同名稱之前,先來看一下 Gson 本身的命名規則是如何~Gson 依照什麼規則去決定欄位的對照呢?

    首先,在 Gson 中預設的命名方式是 FieldNamingPolicy.IDENTITY
    這個命名方式就是欄位名稱要完全一模一樣,Gson 才會幫忙做映射。
    從以下的範例程式來做測試吧。

    public class Quote {
        private long epochDay;
        private String symbolMic;
    
        public void setEpochDay (long value) {
            this.epochDay = value;
        }
    
        public long getEpochDay() {
            return this.epochDay;
        }
    
        public void setSymbolMic(String value) {
            this.symbolMic = value;
        }
    
        public String getSymbolMic() {
            return this.symbolMic;
        }
    }

    主程序則是下面的程式碼:

    private static void testIdentityNaming () {
        Gson gson = new GsonBuilder()
                .setFieldNamingPolicy(FieldNamingPolicy.IDENTITY)
                .create();
            
            JsonObject json;
        Quote quote;
    
        System.out.println("Test 1: all lower-cased key");
        json = new JsonObject();
        json.addProperty("symbolmic", "2331.XTAI");
        json.addProperty("epochday", 16960);
    
        quote = gson.fromJson(json, Quote.class);
        System.out.println("To Object (symbolMic): " + quote.getSymbolMic());
        System.out.println("To Object (epochDate): " + quote.getEpochDay());
    
        System.out.println("\nTest 2: exactly the same key");
        json = new JsonObject();
        json.addProperty("symbolMic", "2331.XTAI");
        json.addProperty("epochDay", 16960);
            
        quote = gson.fromJson(json, Quote.class);
        System.out.println("To Object (symbolMic): " + quote.getSymbolMic());
        System.out.println("To Object (epochDate): " + quote.getEpochDay());
        System.out.println("To Json: " + gson.toJson(quote));
    }

    輸出結果是:

    Test 1: all lower-cased key
    To Object (symbolMic): null
    To Object (epochDate): 0
    
    Test 2: exactly the same key
    To Object (symbolMic): 2331.XTAI
    To Object (epochDate): 16960
    To Json: {"epochDay":16960,"symbolMic":"2331.XTAI"}

    從上述的程式碼可以看出,因為在一開始的物件上,欄位名稱的宣告是 epochDate 和 symbolMic
    因此 Json 要正確地轉成物件的話,Json 的欄位名稱也必須一模一樣(包含大小寫也一樣)才能正常映射。

    其他命名規則

    如果是改用其他命名規則,結果會如何呢?這次試著用 FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES 作為命名規則:

    Gson gson = new GsonBuilder()
    	.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
    	.create();
    
    JsonObject json;
    Quote quote;
    
    System.out.println("Test 3: exactly the same key");
    json = new JsonObject();
    json.addProperty("symbolMic", "2331.XTAI");
    json.addProperty("epochDay", 16960);
    
    quote = gson.fromJson(json, Quote.class);
    System.out.println("To Object (symbolMic): " + quote.getSymbolMic());
    System.out.println("To Object (epochDate): " + quote.getEpochDay());
    
    System.out.println("\nTest 4: all lower-cased with underscores key");
    json = new JsonObject();
    json.addProperty("symbol_mic", "2331.XTAI");
    json.addProperty("epoch_day", 16960);
    
    quote = gson.fromJson(json, Quote.class);
    System.out.println("To Object (symbolMic): " + quote.getSymbolMic());
    System.out.println("To Object (epochDate): " + quote.getEpochDay());
    System.out.println("To Json: " + gson.toJson(quote));

    輸出結果如下:

    Test 3: exactly the same key
    To Object (symbolMic): null
    To Object (epochDate): 0
    
    Test 4: all lower-cased with underscores key
    To Object (symbolMic): 2331.XTAI
    To Object (epochDate): 16960
    To Json: {"epoch_day":16960,"symbol_mic":"2331.XTAI"}

    從結果可以看到,雖然物件上一樣命名是 symbolMic 跟 epochDate
    但改成用 FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES 之後,轉換的規則就變了
    Json 的名稱必須是小寫與底線的組合,才能配對到對應的 Java 變數
    例如 Json 的 epoch_day 會映射到 Java 物件中的 epochDay 變數,以此類推。

    利用 @SerializedName 設定名稱

    在上段的例子中,都是假定 Json 的欄位名稱跟物件的變數名稱是同樣的。
    但當遇到物件的變數名稱,有可能必須要跟 Json 名稱不同時,則可以利用 @SerializedName 來達成。

    舉例來說,如果我希望變數 symbolMic 能夠對應到 Json 欄位 id 的話,可以在 symbolMic 上加上 @SerializedName(“id”)。
    如此一來,Gson 在序列化與反序列化時,就會以 Json 欄位 id 作為填入 symbolMic 的標的。
    範例這裡就先略過了 XD,可以參考 [2] 的範例!

    接著就是主題了~上面講到的方式,全都是序列化與反序列化時都用固定的名字
    例如 @SerializedName(“id”) 表示序列化時,會輸出到 id 這個欄位;反序列化時,則會尋找 id 這個欄位做輸入。
    但如果遇到的是更複雜一點的狀況,序列化成 Json 時要輸出成 id、但反序列化時,卻想要用 symbol_mic 輸入呢?
    這時,就可以利用 @SerializedName 在 Gson 2.4 版之後開始支援的 alternate 屬性來達成了。

    public class Quote {
        @Expose
        @SerializedName(value="date", alternate="epoch_day")
        private long epoch;
    
        @Expose
        @SerializedName(value="id", alternate="symbol_mic")
        private String symbol;
    
        public void setEpochDay (long value) {
            this.epoch = value;
        }
    
        public long getEpochDay() {
            return this.epoch;
        }
    
        public void setSymbolMic(String value) {
            this.symbol = value;
        }
    
        public String getSymbolMic() {
            return this.symbol;
        }
    }

    上面可以看到,對於 epoch 這個變數,被宣告了 value=”date” 與 alternate=”epoch_day” 兩個屬性
    這代表的意思是輸入時,可以使用 date、也可以使用 epoch_day。
    實際要進行轉換時,程式碼如下:

    Gson gson = new GsonBuilder()
        .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
        .create();
        
    JsonObject json = new JsonObject();
    json.addProperty("symbol_mic", "2331.XTAI");
    json.addProperty("epoch_day", 16960);
    
    Quote quote = gson.fromJson(json, Quote.class);
    System.out.println("To Object (symbolMic): " + quote.getSymbolMic());
    System.out.println("To Object (epochDat): " + quote.getEpochDay());
    
    JsonObject deJson = new JsonParser().parse(gson.toJson(quote)).getAsJsonObject();
    System.out.println("To Json: " + deJson);

    一開始先建立一個 Json 物件,內容包含 symbol_mic 和 epoch_day 兩種欄位
    然後呼叫 Gson 去轉換,設定目標為 Quote.class,接著印出轉換後,在 Quote 物件中的兩個變數的值。
    然後再試著用 Gson 把 Quote 物件轉換成 JsonObject,並把 JsonObject 印出來。

    實際執行後的結果如下:

    To Object (symbolMic): 2331.XTAI
    To Object (epochDat): 16960
    To Json: {"date":16960,"id":"2331.XTAI"}

    這個奇妙的需求就達成了!XD
    從 Json 輸入做反序列化時,Gson 是分別使用 symbol_mic 跟 epoch_day 兩個欄位來輸入
    但要從物件輸出做序列化時,Gson 是把 symbol 變數輸出到 id 欄位、epoch 變數輸出到 epoch_day 欄位。

    參考資料
    1. Multiple GSON @SerializedName per field?
    2. GSON Annotations Example
    3. Gson User Guide

    沒有留言: