Swift Json解析探索
客戶端開發專案中,不可避免地需要解析網路資料---將服務端下發的JSON資料解析成客戶端可閱讀友好的Model。Objective-C下使用最多的是JSONModel
,它能在OC Runtime基礎下很好地完成解析工作。那麼在純Swift程式碼中,這個功能是如何實現的?下面開始我們的探索~
- 手動解析
- 原生:Swift4.0 JSONDecoder
JSONDecoder
問題 及 解決方案
手動解析
假設一個User類要解析,Json如下:
{
"userId": 1,
"name": "Jack",
"height": 1.7,
}
複製程式碼
對應的建立一個User結構體(也可以是類):
struct User {
var userId: Int?
var name: String?
var height: CGFloat?
}
複製程式碼
把JSON轉成User
在Swift4.0前,我們以手動解析的方式將JSON model化。給User加一個以JSON為引數的初始化方法,程式碼如下:
struct User {
...
init?(json: [String: Any]) {
guard let userId = json["userId"] as? Int,
let name = json["name"] as? String,
let height = json["height"] as? CGFloat else { return nil }
self.userId = userId
self.name = name
self.height = height
}
}
複製程式碼
依次從json中取出model所需的具體型別的資料,填充到具體對應屬性中。如果其中一個轉換失敗或者沒有值,初始化會失敗返回nil。
如果某個值不需要強校驗,直接取值再賦值,把guard let
內的語句去掉。例如,若height
不用校驗,可看如下程式碼:
struct User {
...
init?(json: [String: Any]) {
guard let userId = json["userId"] as? Int,
let name = json["name"] as? String else { return nil }
self.userId = userId
self.name = name
self.height = json["height"] as? CGFloat
}
}
複製程式碼
原生:Swift4.0 JSONDecoder
2017年6月份左右Swift4.0釋出,其中一個重大更新就是JSON的加解密。擺脫手工解析欄位的繁瑣,聊聊幾行程式碼就可將JSON轉換成Model。與Objective-C下的JSONModel
極為相似。同樣解析上述例子中的User,Swift4.0可以這麼寫:
struct User: Decodable {
var userId: Int?
var name: String?
var height: CGFloat?
}
let decoder = JSONDecoder()
if let data = jsonString.data(using: String.Encoding.utf8) {
let user = try? decoder.decode(User.self, from: data)
}
複製程式碼
so easy~ 與手動解析不同點在於:
-
移除了手寫
init?
方法。不需要手動解了 -
User
實現Decodable
協議,協議的定義如下:/// A type that can decode itself from an external representation. public protocol Decodable { /// Creates a new instance by decoding from the given decoder. /// /// This initializer throws an error if reading from the decoder fails, or /// if the data read is corrupted or otherwise invalid. /// /// - Parameter decoder: The decoder to read data from. public init(from decoder: Decoder) throws } 複製程式碼
Decodable
協議只有一個方法public init(from decoder: Decoder) throws
---以Decoder
例項進行初始化,初始化失敗可能丟擲異常。慶幸的是,只要繼承Decodable
協議,系統會自動檢測類中的屬性進行初始化工作,省去了人工解析的麻煩~ -
使用了
JSONDecoder
。它是真正的解析工具,主導整個解析過程
讀到這裡,是不是覺得人生從黑暗邁向了光明~~
可是,它並不完美...
JSONDecoder問題及方案
解析JSON經常遇到這樣兩種不一致問題:
- 服務端下發的key跟端上不一致。比如,服務端下發key="order_id",端上定義key="orderId"
- 服務端下發的日期表達是
yyyy-MM-dd HH:mm
或者時間戳,但端上是Date
型別 - 服務端下發的基本型別和端上定義的不一致。服務端下發的是
String
,端上定義的Int
,等
前兩個問題JSONDecoder
都能很好地解決。
第一個key不一致問題,JSONDecoder
有現成的方案。以上面介紹的例子來說,假設服務端返回的key
是user_id
而不是userId
,那麼我們可以使用JSONDecoder
的CodingKeys
像JSONModel
一樣對屬性名稱在加解密時的名稱做轉換。User
修改如下:
struct User: Decodable {
var userId: Int?
var name: String?
var height: CGFloat?
enum CodingKeys: String, CodingKey {
case userId = "user_id"
case name
case height
}
}
複製程式碼
第二個,Date
轉換問題。JSONDecoder
也為我們提供了單獨的API:
open class JSONDecoder {
/// The strategy to use for decoding `Date` values.
public enum DateDecodingStrategy {
/// Defer to `Date` for decoding. This is the default strategy.
case deferredToDate
/// Decode the `Date` as a UNIX timestamp from a JSON number.
case secondsSince1970
/// Decode the `Date` as UNIX millisecond timestamp from a JSON number.
case millisecondsSince1970
/// Decode the `Date` as an ISO-8601-formatted string (in RFC 3339 format).
case iso8601
/// Decode the `Date` as a string parsed by the given formatter.
case formatted(DateFormatter)
/// Decode the `Date` as a custom value decoded by the given closure.
case custom((Decoder) throws -> Date)
}
......
/// The strategy to use in decoding dates. Defaults to `.deferredToDate`.
open var dateDecodingStrategy: JSONDecoder.DateDecodingStrategy
}
複製程式碼
設定好了JSONDecoder
屬性dateDecodingStrategy
後,解析Date
型別就會按照指定的策略進行解析。
型別不一致
至此,JSONDecoder
為我們提供了
- 解析不同
key
值物件 Date
型別可自定義轉換Float
在一些正負無窮及無值得特殊表示。(出現的概率很少,不作具體說明了)
但遇到基本型別端上與服務端不一致時(比如一個數字1,端上的Code是Int型,服務端下發String:"1"),JSONDecoder
會丟擲typeMismatch
異常而終結整個資料的解析。
這讓人有點懊惱,端上的應用,我們希望它能夠儘可能穩定,而不是某些情況下遇到若干個基本型別不一致整個解析就停止,甚至是 Crash。
如下面表格所示,我們希望型別不匹配時,能夠這麼處理:左列代表前端的型別,右列代表服務端型別,每一行代表前端型別為X時,能從服務端下發的哪些型別中轉化,比如String
可以從 Int
orFloat
轉化。這幾個型別基本能覆蓋日常服務端下發的資料,其它型別的轉化可根據自己的需求擴充。
前端 | 服務端 |
---|---|
String | Int,Float |
Float | String |
Double | String |
Bool | String, Int |
JSONDecoder
沒有給我們便利的這種異常處理的API。如何解決呢?最直接的想法,在具體的model
內實現init(decoder: Decoder)
手動解析可以實現,但每個都這麼處理太麻煩。
解決方案:KeyedDecodingContainer
方法覆蓋
研究JSONDecoder的原始碼,在解析自定義Model過程中,會發現這樣一個呼叫關係。
// 入口方法
JSONDecoder decoder(type:Type data:Data)
// 內部類,真實用來解析的
_JSONDecoder unbox(value:Any type:Type)
// Model呼叫init方法
Decodable init(decoder: Decoder)
// 自動生成的init方法呼叫container
Decoder container(keyedBy:CodingKeys)
// 解析的容器
KeyedDecodingContainer decoderIfPresent(type:Type) or decode(type:Type)
// 內部類,迴圈呼叫unbox
_JSONDecoder unbox(value:Any type:Type)
...迴圈,直到基本型別
複製程式碼
最終的解析落到,_JSONDecoder
的unbox
及 KeyedDecodingContainer
的decoderIfPresent decode
方法。但_JSONDecoder
是內部類,我們處理不了。最終決定對KeyedDecodingContainer
下手,其中部分程式碼如下:
extension KeyedDecodingContainer {
.......
/// Decode (Int, String) -> Int if possiable
public func decodeIfPresent(_ type: Int.Type, forKey key: K) throws -> Int? {
if let value = try? decode(type, forKey: key) {
return value
}
if let value = try? decode(String.self, forKey: key) {
return Int(value)
}
return nil
}
.......
/// Avoid the failure just when decoding type of Dictionary, Array, SubModel failed
public func decodeIfPresent<T>(_ type: T.Type, forKey key: K) throws -> T? where T : Decodable {
return try? decode(type, forKey: key)
}
}
複製程式碼
上述程式碼中,第一個函式decodeIfPresent(_ type: Int.Type, forKey key: K)
是以key
的資訊解析出Int?
值。這裡覆蓋了KeyedDecodingContainer
中的該函式的實現,現在已try?
的形式以Int
型別解析,解析成功則直接返回,失敗則以String
型別解析出一個StringValue,如果解析成功,再把String
轉換成Int?
值。
為什麼要寫第二個函式呢?
場景:當我們Model內有其他的非基本型別的Model,比如其他自定義Model,Dictionary<String, Any>
,Array<String>
等,當這些Model 型別不匹配或者出錯誤時也會丟擲異常,導致整個大Model解析失敗。
覆蓋decodeIfPresent<T>(_ type: T.Type, forKey key: K)
可以避免這些場景。至此,當型別過程中出現解析的Optional型別出現不匹配時,我們要不是通過轉換,要不就是給其賦值nil
,避免了系統此時直接throw exception
導致退出整個解析過程的尷尬。
為何不覆蓋
decode
方法?decodeIfPresent
可以返回Optional值,decode
返回確定型別值。考慮到如果Model內如果定義的型別是No-Optional型,那麼可以認為開發者確定該值必須存在,如果不存在Model很可能是錯誤的,所以直接fail。
總結
Swift4.0 JSONDecoder
確實為解析資料帶來了極大的便利。使用方式上類似Objective-C下的JSONModel
。但實際開發中還是需要一些改造才能更好地服務於我們。
對文章有任何疑問或者有想探討的問題,隨時留言溝通~