采用Gson解析含有多種JsonObject的復雜json

本文對應的項目是MultiTypeJsonParser 女揭,項目地址 https://github.com/sososeen09/MultiTypeJsonParser

0 前奏

使用 Gson 去解析 json 應該是很常見的子姜,大部分的情況下我們只要創(chuàng)建一個 Gson 對象,然后根據(jù) json 和對應的 Java 類去解析就可以了灶壶。

Gson gson = new Gson();
Person person = gson.form(json,Person.class);

但是對于比較復雜的 json义郑,比如下面這種蝶柿, attributes 對應的 jsonObject 中的字段是完全不一樣的,這個時候再簡單的用上面的方法就解析不了了非驮。

{
    "total": 2,
    "list": [
        {
            "type": "address",
            "attributes": {
                "street": "NanJing Road",
                "city": "ShangHai",
                "country": "China"
            }
        },
        {
            "type": "name",
            "attributes": {
                "first-name": "Su",
                "last-name": "Tu"
            }
        }
    ]
}

當然了交汤,我們說一步到位的方式解決不了,但用一點笨方法還是可以的劫笙。比如先手動解析拿到 attributes 對應的 jsonObject芙扎,根據(jù)與它同級 type 對應的 value 就可以判斷這一段 jsonObject 對應的 Java 類是哪個,最后就采用 gson.from() 方法解析出 attributes 對應的 Java 對象填大。


ListInfoWithType listInfoWithType = new ListInfoWithType();

//創(chuàng)建 org.json 包下的 JSONObject 對象
JSONObject jsonObject = new JSONObject(TestJson.TEST_JSON_1);
int total = jsonObject.getInt("total");

//創(chuàng)建 org.json 包下的 JSONArray 對象
JSONArray jsonArray = jsonObject.getJSONArray("list");
Gson gson = new Gson();
List<AttributeWithType> list = new ArrayList<>();

//遍歷
for (int i = 0; i < jsonArray.length(); i++) {
    JSONObject innerJsonObject = jsonArray.getJSONObject(i);
    Class<? extends Attribute> clazz;
    String type = innerJsonObject.getString("type");
    if (TextUtils.equals(type, "address")) {
        clazz = AddressAttribute.class;
    } else if (TextUtils.equals(type, "name")) {
        clazz = NameAttribute.class;
    } else {
        //有未知的類型就跳過
        continue;
    }
    AttributeWithType attributeWithType = new AttributeWithType();

//采用Gson解析
    Attribute attribute = gson.fromJson(innerJsonObject.getString("attributes"), clazz);
    attributeWithType.setType(type);
    attributeWithType.setAttributes(attribute);
    list.add(attributeWithType);
}

listInfoWithType.setTotal(total);
listInfoWithType.setList(list);

雖然這樣能實現(xiàn)整個 json 的反序列化纵顾,但是這種方式比較麻煩,而且一點也不優(yōu)雅栋盹,如果項目中存在很多這樣的情況,就會做很多重復的體力勞動敷矫。
如何更優(yōu)雅例获、更通用的解決這類問題,在網(wǎng)上沒有找到答案曹仗,只好去深入研究一下Gson了榨汤。帶著這樣的目的,翻看了Gson的文檔怎茫,發(fā)現(xiàn)了一句話

Gson can work with arbitrary Java objects including pre-existing objects that you do not have source code of.

這句話說 Gson 可以處理任意的 Java 對象收壕。那么對于上面講的那種反序列化情況來講, Gson 應該也能做到轨蛤。通過研究 Gson 的文檔蜜宪,發(fā)現(xiàn)可以通過自定義JsonDeserializer的方式來實現(xiàn)解析這種 jsonObject 類型不同的情況。

我們知道祥山,大部分情況下 Gson 是通過直接 new 出來的方式來創(chuàng)建圃验,不過也可以采用 GsonBuilder 這個類去生成 Gson。

  Gson gson = new GsonBuilder()
   .registerTypeAdapter(Id.class, new IdTypeAdapter())
   .enableComplexMapKeySerialization()
   .serializeNulls()
   .setDateFormat(DateFormat.LONG)
   .setFieldNamingPolicy(FieldNamingPolicy.UPPER_CAMEL_CASE)
   .setPrettyPrinting()
   .setVersion(1.0)
   .create();

GsonBuilder 通過 registerTypeAdapter()方法缝呕,對目標類進行注冊澳窑。當序列化或者反序列化目標類的時候就會調用我們注冊的typeAdapter斧散, 這樣就實現(xiàn)了人工干預 Gson 的序列化和反序列化過程。

GsonBuilder 的 registerTypeAdapte() 方法的第二個參數(shù)是 Object 類型摊聋,也就意味著我們可以注冊多種類型的 typeAdapter鸡捐,目前支持的類型有 JsonSerializer、JsonDeserializer麻裁、InstanceCreator箍镜、TypeAdapter。

  public GsonBuilder registerTypeAdapter(Type type, Object typeAdapter) 

經(jīng)過一番搗鼓悲立,寫了一個工具類鹿寨,對于上面的那個復雜 json,用了不到10行代碼就搞定薪夕,而且比較優(yōu)雅和通用脚草。

MultiTypeJsonParser<Attribute> multiTypeJsonParser = new MultiTypeJsonParser.Builder<Attribute>()
        .registerTypeElementName("type")
        .registerTargetClass(Attribute.class)
        .registerTargetUpperLevelClass(AttributeWithType.class)
        .registerTypeElementValueWithClassType("address", AddressAttribute.class)
        .registerTypeElementValueWithClassType("name", NameAttribute.class)
        .build();

ListInfoWithType listInfoWithType = multiTypeJsonParser.fromJson(TestJson.TEST_JSON_1, ListInfoWithType.class);

本文就簡單分析一下如何通過自定義 JsonDeserializer 來實現(xiàn)一個通用的工具類用于解析復雜類型 json。對于以后碰到相似問題原献,這種處理方法可以提供一種解決問題的思路馏慨。具體的代碼和實例,可以查看項目姑隅。如果對您的思路有一些啟發(fā)写隶,歡迎交流和Star。

1 JsonDeserializer介紹

JsonDeserializer 是一個接口讲仰,使用的時候需要實現(xiàn)這個接口并在 GsonBuilder 中對具體的類型去注冊慕趴。當反序列化到對應的類的時候就會調用這個自定義 JsonDeserializer 的 deserialize() 方法。下面對這個方法的幾個參數(shù)做一下解釋鄙陡,以便于更好的理解Gson解析的過程冕房。

public interface JsonDeserializer<T> {
  public T deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
      throws JsonParseException;
}

1.1 JsonElement

JsonElement代表 在 Gson 中的代表一個元素。它是一個抽象類趁矾,有4個子類:JsonObject耙册、JsonArray、JsonPrimitive毫捣、JsonNull详拙。
1.JsonObject 表示的是包含name-value型的 json 字符串,其中 name 是字符串蔓同,而 value 可以是其它類型的 JsonElement 元素饶辙。在json中用 “{}” 包裹起來的一個整體就是JsonObject。例如

// "attributes" 是name斑粱,后面跟著的{}內容是它對應的value畸悬,而這個value就是一個JsonObject
  "attributes": {
                  "first-name": "Su",
                  "last-name": "Tu"
                 }

2.JsonArray 這個類在 Gson 中代表一個數(shù)組類型,一個數(shù)組就是JsonElement的集合,這個集合中每一個類型都可能不同蹋宦。這是一個有序的集合披粟,意味著元素的添加順序是被維持著的。上面例子中l(wèi)ist對應的 “[]” 包裹起來的json就是JsonArray冷冗。

3.**JsonPrimitive ** 這個可以認為是json中的原始類型的值守屉,包含Java的8個基本類型和它們對應的包裝類型,也包含 String 類型蒿辙。比如上面 "first-name" 對應的 "Su" 就是一個 String 類型的 JsonPrimitive 拇泛。

4.JsonNull 通過名字也可以猜到,這個代表的是 null 值思灌。

1.2 Type

Type是Java中的所有類型的頂層接口俺叭,它的子類有 GenericArrayType、ParameterizedType泰偿、TypeVariable熄守、WildcardType,這個都是在java.lang.reflect包下面的類耗跛。另外裕照,我們最熟悉的一個類 Class 也實現(xiàn)了 Type 接口。

一般來講调塌,調用 GsonBuilder 的 registerTypeAdapter() 去注冊晋南,第一個參數(shù)使用 Class 類型就可以了。

1.3 JsonDeserializationContext

這個類是在反序列過程中羔砾,由其它類調用我們自定義的 JsonDeserialization 的 deserialize() 方法時傳遞過來的负间,在 Gson 中它唯一的一個實現(xiàn)是TreeTypeAdapter 中的一個私有的內部類 GsonContextImpl 〗啵可以在自定義的 JsonDeserializer 的 deserialize() 中去調用 JsonDeserializationContext 的 deserialize() 方法去獲得一個對象政溃。

但是要記住,如果傳遞到 JsonDeserializationContext 中的 json 與 JsonDeserializer 中的 json 一樣的話檀葛,可能會導致死循環(huán)調用。

2 思路分析

2.1 創(chuàng)建JavaBean

還是以最上面的那個 json 進行分析腹缩,在 list 對應 JsonArray 屿聋,其中的兩個 JsonObject 中,attributes 對應的 JsonObject 字段完全不一樣藏鹊,但是為了統(tǒng)一润讥,在寫 JavaBean 的時候可以給它們設置一個共同的父類,盡管它是空的盘寡。

public class Attribute {
      ...
}

public class AddressAttribute extends Attribute {
    private String street;
    private String city;
    private String country;
... 省略get/set
}

public class NameAttribute extends Attribute {
    @SerializedName("first-name")
    private String firstname;
    @SerializedName("last-name")
    private String lastname;
...省略get/set
}

設置 Attribute 這個 SuperClass 只是為了在 GsonBuilder 去注冊楚殿,當具體解析的時候我們會根據(jù)
type 對應的類型去找到對應的Class。

 gsonBuilder.registerTypeAdapter(Attribute.class, new AttributeJsonDeserializer());

到了這里我們就應該想到竿痰,type 對應的 value 肯定是要與具體的 JavaBean 對應起來的脆粥。比如在這里就是

"address"——AddressAttribute.class
"name"——NameAttribute.class

如果 type 是 "address" 砌溺,那么我們就可以用 gson 去拿 AddressAttribute.class 和對應的 json 去解析。

Attribute attribute = gson.form(addressJson,AddressAttribute.class);

2.2 如何把 json 準確的轉為對應的 JavaBean

我們注冊的是父類 Attribute 变隔,當反序列化需要解析 Attribute 的時候就會把對應的 json 作為參數(shù)回調自定義的 JsonDeserializer 规伐。我們就可以在下面這個方法中寫自己的邏輯得到我們需要的 Attribute 對象了。

 public Attribute deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)

但是細心的朋友應該會發(fā)現(xiàn)了匣缘,這個時候傳遞的 json 有可能是這樣的

{
   "street": "NanJing Road",
   "city": "ShangHai",
   "country": "China"
}

也有可能是這樣的

{
   "first-name": "Su",
   "last-name": "Tu"
}

我們怎么知道該解析成 AddressAttribute 還是 NameAttribute 猖闪??肌厨?

我們想想培慌,具體解析成哪個,我們肯定是需要知道 type 對應的 value 柑爸。而這個 type 是與 attributes 同級的字段吵护,照著剛才這樣肯定是沒希望拿到這個 value 的。

我們再想想竖配,能夠知道這個 type 對應的 value 是什么的肯定是 attributes 上一層級的 json 何址。

{
   "type": "name",
   "attributes": {
                          ...
                 }  
}

那么我們可不可以在 GsonBuilder 中再去注冊一個 typeAdapter 來解析這個外層的 json 呢?當然可以进胯。

 gsonBuilder.registerTypeAdapter(AttributeWithType.class, new AttributeWithTypeJsonDeserializer());

這個 AttributeWithType 就是外層的 json 對應的 JavaBean

public class AttributeWithType {
    private String type;
    private Attribute attributes;
     ...
}

在反序列化 AttributeWithType 這個類的時候用爪,我們可以獲得這個 type 對應的 value,然后把這個 value 傳遞給里層的 Attribute 對應的 JsonDeserializer胁镐。這樣就可以根據(jù) value 是 “address” 或者 “name” 去對 AddresAttribute 或者 NameAttribute 進行反序列化了偎血。

2.3 有一個坑

前面那我們講過,調用 JsonDeserializationContext 的方法應該注意死循環(huán)盯漂。在具體的實踐中颇玷,我雖然沒有調用 JsonDeserializationContext 的方法,但是依然出現(xiàn)了死循環(huán)的情況就缆。就是因為我是這么用的帖渠。

 AttributeWithType attributeWithType = gson.fromJson(json, AttributeWithType.class);

乍一看沒什么問題啊,問題就出在這個 gson 身上竭宰。這個 gson 是已經(jīng)注冊過解析 AttributeWithType 的 GsonBuilder 創(chuàng)建的空郊。 gson.fromJson() 方法中的 json 是 AttributeWithType 對應的反序列化的 json,gson.fromJson() 內部會再次調用 AttributeWithType 對應的 JsonDeserializer 中的 deserialize() 方法切揭,從而導致死循環(huán)狞甚。

避免死循環(huán)的方式就是用GsonBuilder新建一個 gson ,這個GsonBuilder不再注冊 AttributeWithType 廓旬,而只去注冊 Attribute 去解析哼审。

3 為了更好更通用

1.在項目中,可能還會存在另一種格式的json,外部沒有單獨的type元素涩盾,而是與其它的元素放在同一個JsonObject中十气。這樣的格式更省事,不需要注冊外層的typeAdaper即可旁赊。

{
    "total": 2,
    "list": [
        {
            "type": "address",
            "street": "NanJing Road",
            "city": "ShangHai",
            "country": "China"
        },
        {
            "type": "name",
            "first-name": "Su",
            "last-name": "Tu"
        }
    ]
}

MultiTypeJsonParser<Attribute> multiTypeJsonParser = new MultiTypeJsonParser.Builder<Attribute>()
        .registerTypeElementName("type")
        .registerTargetClass(Attribute.class)
// 如果所要解析的 jsonObejct 中已經(jīng)含有能夠表示自身類型的字段桦踊,不需要注冊外層 Type,這樣更省事
//        .registerTargetUpperLevelClass(AttributeWithType.class)
        .registerTypeElementValueWithClassType("address", AddressAttribute.class)
        .registerTypeElementValueWithClassType("name", NameAttribute.class)
        .build();

ListInfoWithType listInfoWithType = multiTypeJsonParser.fromJson(TestJson.TEST_JSON_1, ListInfoWithType.class);

2.如果在解析過程中發(fā)現(xiàn)有些類型沒有注冊到 MultiTypeJsonParser 的 Builder 中终畅,解析的時候碰到相應的 jsonObject 就直接返回null籍胯。比如下面這樣的json中,"type" 對應的 "parents" 如果沒有注冊离福,那么反序列化的時候這個 json 所代表的對象就為 null 杖狼。

 {
        "type": "parents",
        "attributes": {
          "mather": "mi lan",
          "father": "lin ken"
        }
 }

在Android中我們反序列這樣的 json 后一般會把得到的對象的設置到列表控件上,如果后端返回的 json 中包含之前未注冊的類型妖爷,為了程序不至于 crash蝶涩,需要對反序列化的 null 對象進行過濾,項目中提供了一個工具類 ListItemFilter 可以過濾集合中為 null 的元素絮识。

4 結語

對于如何優(yōu)雅的解析這種類型不同的 JsonObject 绿聘,剛開始我是缺少思路的,在網(wǎng)上也沒有查到合適的文檔次舌。但是通過查看 Gson 的文檔和源碼熄攘,通過自己的理解和分析,逐步的完成了這個過程彼念。我的一個感觸就是挪圾,多去看看官方的使用文檔應該比盲目去搜索解決方案更好。

代碼是最好的文檔逐沙,本文只簡單介紹了一些實現(xiàn)思路哲思,文中貼出的一些代碼是為了講述方便,與項目中的代碼可能會有有些區(qū)別吩案。具體的使用可以看項目中的例子棚赔。

如果有問題,歡迎提 issue 或留言徘郭,如果對您有所幫助靠益,歡迎Star。

參考

Gson官方文檔

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末崎岂,一起剝皮案震驚了整個濱河市捆毫,隨后出現(xiàn)的幾起案子闪湾,更是在濱河造成了極大的恐慌冲甘,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異江醇,居然都是意外死亡濒憋,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進店門陶夜,熙熙樓的掌柜王于貴愁眉苦臉地迎上來凛驮,“玉大人,你說我怎么就攤上這事条辟∏玻” “怎么了?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵羽嫡,是天一觀的道長本姥。 經(jīng)常有香客問我,道長杭棵,這世上最難降的妖魔是什么婚惫? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任,我火速辦了婚禮魂爪,結果婚禮上先舷,老公的妹妹穿的比我還像新娘。我一直安慰自己滓侍,他們只是感情好蒋川,可當我...
    茶點故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著粗井,像睡著了一般尔破。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上浇衬,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天懒构,我揣著相機與錄音,去河邊找鬼耘擂。 笑死胆剧,一個胖子當著我的面吹牛,可吹牛的內容都是我干的醉冤。 我是一名探鬼主播秩霍,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼蚁阳!你這毒婦竟也來了铃绒?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤螺捐,失蹤者是張志新(化名)和其女友劉穎颠悬,沒想到半個月后矮燎,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡赔癌,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年诞外,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片灾票。...
    茶點故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡峡谊,死狀恐怖,靈堂內的尸體忽然破棺而出刊苍,到底是詐尸還是另有隱情既们,我是刑警寧澤,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布正什,位于F島的核電站贤壁,受9級特大地震影響,放射性物質發(fā)生泄漏埠忘。R本人自食惡果不足惜脾拆,卻給世界環(huán)境...
    茶點故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望莹妒。 院中可真熱鬧名船,春花似錦、人聲如沸旨怠。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽鉴腻。三九已至迷扇,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間爽哎,已是汗流浹背蜓席。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留课锌,地道東北人厨内。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓,卻偏偏與公主長得像渺贤,于是被迫代替她去往敵國和親雏胃。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,722評論 2 345

推薦閱讀更多精彩內容