""轉 Int,{} 轉 List,還有什麼奇葩的 JSON 要容錯?| 實戰

一. 序
前幾天寫了一篇,關於利用 GSON 在 JSON 序列化和反序列化之間,資料容錯的文章。最簡單的利用 @SerializedName
註解來配置多個不同 JSON Key 值,或者再使用 @Expose
來配置一些例外的情況。更復雜一些的資料,可以使用 TypeAdapter 來解決,TypeAdapter 可以說是一顆 GSON 解析 JSON 的銀彈,所有複雜資料解析以及容錯問題,都可以通過它來解決。還不瞭解的可以先看看之前的文章《 利用 Gson 做好 JSON 資料容錯 》。
文章評論裡和公眾號後臺有一些小夥伴,針對具體資料容錯的場景,提出了具體的問題。例如空串(“”)如何轉 Int,物件({}) 轉 List。
今天就在這篇文章裡統一解答,並且給出解決方案。
二. GSON 資料容錯例項
就像前文中介紹的一樣,GSON 已經提供了一些簡單的註解,去做資料的容錯處理。更復雜的操作,就需要用到 TypeAdapter 了,需要注意的是,一旦上了 TypeAdapter 之後,註解的配置就會失效。
2.1 什麼是 TypeAdapter
TypeAdapter 是 GSON 2.1 版本開始支援的一個抽象類,用於接管某些型別的序列化和反序列化。TypeAdapter 最重要的兩個方法就是 write()
和 read()
,它們分別接管了序列化和反序列化的具體過程。
如果想單獨接管序列化或反序列化的某一個過程,可以使用 JsonSerializer 和 JsonDeserializer 這兩個介面,它們組合起來的效果和 TypeAdapter 類似,但是其內部實現是不同的。
簡單來說,TypeAdapter 支援 Stream,所以它比較省記憶體,但是使用起來有些不方便。而 JsonSerializer 和 JsonDeserializer 是將資料都讀到記憶體中再進行操作,會比 TypeAdapter 更費記憶體,但是 API 使用起來更清晰一些。
雖然 TypeAdapter 更省記憶體,但是通常我們業務介面傳輸的那點資料量,所佔用的記憶體其實影響不大,可以忽略不計。
因為 TypeAdapter、JsonSerializer 以及 JsonDeserializer 都需要配合 GsonBuilder.registerTypeAdapter()
方法,所以在本文中,此種接管方式,統稱為 TypeAdapter 接管。
2.2 空字串轉 0
對於一些強轉有效的型別轉換,GSON 本身是有一些預設的容錯機制的。比如:將字串 “18” 轉換成 Java 中整型的 18,這是被預設支援的。
例如我有一個記錄使用者資訊的 User 類。
class User{ var name = "" var age = 0 override fun toString(): String { return """ { "name":"${name}", "age":${age} } """.trimIndent() } }
User 類中包含 name
和 age
兩個欄位,其中 age
對應的 JSON 型別,可以是 18
也可以是 "18"
,這都是允許的。
{ "name":"承香墨影", "age":18 // "age":"18" }
那假如服務端說,這個使用者沒有填年齡的資訊,所以直接返回了一個空串 ""
,這個時候客戶端用 Gson 解析就悲劇了。
這當然是服務端的問題,如果資料明確為 Int 型別,那麼就算是預設值也應該是 0 或者 -1。
但遇到這樣的情況,你還用預設的 GSON 策略去解析,你將得到一個 Crash。
Caused by: com.google.gson.JsonSyntaxException: - java.lang.NumberFormatException: --empty String
沒有一點意外也沒有一點驚喜的 Crash 了,那接下來看看如何解決這樣的資料容錯問題?
因為這裡的場景中,只需要反序列化的操作,所以我們實現 JsonDeserializer 介面即可,接管的是 Int 型別。直接上例子吧。
class IntDefaut0Adapter : JsonDeserializer<Int> { override fun deserialize(json: JsonElement?, typeOfT: Type?, context: JsonDeserializationContext?): Int { if (json?.getAsString().equals("")) { return 0 } try { return json!!.getAsInt() } catch (e: NumberFormatException) { return 0 } } } fun intDefault0(){ val jsonStr = """ { "name":"承香墨影", "age":"" } """.trimIndent() val user = GsonBuilder() .registerTypeAdapter( Int::class.java, IntDefaut0Adapter()) .create() .fromJson<User>(jsonStr,User::class.java) Log.i("cxmydev","user: ${user.toString()}") }
在 IntDefaut0Adapter 中,首先判斷資料字串是否為空字串 ""
,如果是則直接返回 0,否則將其按 Int 型別解析。在這個例子中,將整型 0 作為一個異常引數進行處理。
2.3 null、[]、List 轉 List
還有一些小夥伴比較關心的,對於 JSONObject 和 JSONArray 相容的問題。
例如需要返回一個 List,翻譯成 JSON 資料就應該是方括號 []
包裹的 JSONArray。但是在列表為空的時候,服務端返回的資料,什麼情況都有可能。
{ "name":"承香墨影", "languages":["EN","CN"] // 理想的資料 // "languages":"" // "languages":null // "languages":{} }
例子的 JSON 中, languages
欄位表示當前使用者所掌握的語言。當語言欄位沒有被設定的時候,服務端返回的資料不一致,如何相容呢?
我們在前文的 User 類中,增加一個 languages 的欄位,型別為 ArrayList
var languages = ArrayList<String>()
在 Java 中,列表集合都會實現 List 介面,所以我們在實現 JsonDeserializer 的時候,解析攔截的應該是 List。
在這個情況下,可以使用 JsonElement 的 isJsonArray()
方法,判斷當前是否是一個合法的 JSONArray 的陣列,一旦不正確,就直接返回一個空的集合即可。
class ArraySecurityAdapter:JsonDeserializer<List<*>>{ override fun deserialize(json: JsonElement, typeOfT: Type?, context: JsonDeserializationContext?): List<*> { if(json.isJsonArray()){ val newGson = Gson() return newGson.fromJson(json, typeOfT) }else{ return Collections.EMPTY_LIST } } } fun listDefaultEmpty(){ val jsonStr = """ { "name":"承香墨影", "age":"18", "languages":{} } """.trimIndent() val user = GsonBuilder() .registerTypeHierarchyAdapter( List::class.java, ArraySecurityAdapter()) .create() .fromJson<User>(jsonStr,User::class.java) Log.i("cxmydev","user: ${user.toString()}") }
其核心就是 isJsonArray()
方法,判斷當前是否是一個 JSONArray,如果是,再具體解析即可。到這一步就很靈活了,你可以直接用 Gson 將資料反序列化成一個 List,也可以將通過一個 for 迴圈將其中的每一項單獨反序列化。
需要注意的是,如果依然想用 Gson 來解析,需要重新建立一個新的 Gson 物件,不可以直接複用 JsonDeserializationContext,否則會造成遞迴呼叫。
另外還有一個細節,在這個例子中,呼叫的是 registerTypeHierarchyAdapter()
方法來註冊 TypeAdapter,它和我們前面介紹的 registerTypeAdapter()
有什麼區別呢?
通常我們會根據不同的場景,選擇不同資料結構實現的集合類,例如 ArrayList 或者 LinkedList。但是 registerTypeAdapter()
方法,要求我們傳遞一個明確的型別,也就是說它不支援繼承,而 registerTypeHierarchyAdapter()
則可以支援繼承。
我們想用 List 來替代所有的 List 子類,就需要使用 registerTypeHierarchyAdapter()
方法,或者我們的 Java Bean 中,只使用 List。這兩種情況都是可以的。
2.4 保留原 Json 字串
看到這個小標題,可能會有疑問,保留原 Json 字串是一個什麼情況?得到的 Json 資料,本身就是一個字串,且挺我細細說來。
舉個例子,前面定義的 User 類,需要存到 SQLite 資料庫中,語言( languages )欄位也是需要儲存的。說到 SQLite,當然優先使用一些開源的 ORM 框架了,而不少優秀的 ORM-SQLite 框架,都通過外來鍵的形式支援了一對多的儲存。例如一篇文章對應多條評論,一條使用者資訊對應對應多條語言資訊。
這種場景下我們當然可以使用 ORM 框架本身提供的一對多的儲存形式。但是如果像現在的例子中,只是簡單的儲存一些有限的資料,例如使用者會的語言( languages )。這種簡單的有限資料,用外來鍵有一些偏重了。
此時我們就想,要是可以直接在 SQLite 中儲存 languages 欄位的 JSON,將其當成一個字串去儲存,是不是就簡單了?把一個多級的結構拉平成一級,剩下的只需要擴展出一個反序列化的方法,對業務來說,這些操作都是透明的。
那拍腦袋想,如果 Gson 有簡單的容錯,那我們將這個解析的欄位型別定義成 String,是不是就可以做到了?
@SerializedName("languages") var languageStr = ""
很遺憾,這並沒有辦法做到,如果你這樣使用,你將得到一個 IllegalStateException 的異常。
Caused by: com.google.gson.JsonSyntaxException: java.lang.IllegalStateException: Expected a string but was BEGIN_ARRAY at line 4 column 18 path $.languages
之所以會出現這樣的情況,簡單來說,雖然 deserialize()
方法傳遞的引數都是 JsonElement,但是 JsonElement 只是一個抽象類,最終會根據資料的情況,轉換成它的幾個實現類的其中之一,這些實現類都是 final class,分別是 JsonObject、JsonArray、JsonPrimitive、JsonNull,這些從命名上就很好理解了,它們代表了不通的 JSON 資料,就不過多介紹了。
使用了 Gson 之後,遇到花括號 {}
會生成一個 JsonObject,而字串則是基本型別的 JsonPrimitive 物件,它們在 Gson 內部的解析流程是不一樣的,這就造成了 IllegalStateException 異常。
那麼接下來看看如何解決這個問題。
既然 TypeAdapter 是 Gson 解析的銀彈,找不到解決方案,用它就對了。思路繼續是用 JsonDeserializer 來接管解析,這一次將 User 類的整個解析都接管了。
class UserGsonAdapter:JsonDeserializer<User>{ override fun deserialize(json: JsonElement, typeOfT: Type?, context: JsonDeserializationContext?): User { var user = User() if(json.isJsonObject){ val jsonObject = JSONObject(json.asJsonObject.toString()) user.name = jsonObject.optString("name") user.age = jsonObject.optInt("age") user.languageStr = jsonObject.optString("languages") user.languages = ArrayList() val languageJsonArray = JSONArray(user.languageStr) for(i in 0 until languageJsonArray.length()){ user.languages.add(languageJsonArray.optString(i)) } } return user } } fun userGsonStr(){ val jsonStr = """ { "name":"承香墨影", "age":"18", "languages":["CN","EN"] } """.trimIndent() val user = GsonBuilder() .registerTypeAdapter( User::class.java, UserGsonAdapter()) .create() .fromJson<User>(jsonStr,User::class.java) Log.i("cxmydev","user: \n${user.toString()}") }
在這裡我直接使用標準 API org.json 包中的類去解析 JSON 資料,當然你也可以通過 Gson 本身提供的一些方法去解析,這裡只是提供一個思路而已。
最終 Log 輸出的效果如下:
{ "name":"承香墨影", "age":18, "languagesJson":["CN","EN"], "languages size:"2 }
在這個例子中,最終解析還是使用了標準的 JSONObject 和 JSONArray 類,和 Gson 沒有任何關係,Gson 只是起到了一個橋接的作用,好像這個例子也沒什麼實際用處。
不談場景說應用都是耍流氓,那麼如果是使用 Retrofit 呢?Retrofit 可以配置 Gson 做為資料的轉換器,在其內部就完成了反序列化的過程。這種情況,配合 Gson 的 TypeAdapter,就不需要我們在額外的編寫解析的程式碼了,網路請求走一套邏輯即可。
如果覺得在構造 Retrofit 的時候,為 Gson 新增 TypeAdapter 有些入侵嚴重了,可以配合 @JsonAdapter
註解使用。
三. 小結時刻
針對服務端返回資料的容錯處理,很大一部分其實都是來自雙端沒有保證資料一致的問題。而針對開發者來說,要做到外部資料均不可信的,客戶端不信本地讀取的資料、不信服務端返回的資料,服務端也不能相信客戶端傳遞的資料。這就是所謂防禦式程式設計。
言歸正傳,我們小結一下本文的內容:
-
TypeAdapter( 包含JsonSerializer、JsonDeserializer ) 是 Gson 解析的銀彈,所有 Json 解析的定製化要求都可以通過它來實現。
-
registerTypeAdapter()
方法需要制定確定的資料型別,如果想支援繼承,需要使用registerTypeHierarchyAdapter()
方法。 -
如果資料量不大,推薦使用 JsonSerializer 和 JsonDeserializer。
-
針對整個 Java Bean 的解析接管,可以使用
@JsonAdapter
註解。
就這樣吧,還有什麼奇葩的 JSON 場景,歡迎在推文文末留言。
本文對你有幫助嗎? 留言、點贊、轉發 是最大的支援,謝謝!