Swift Json解析探索

Swift Json解析探索

客戶端開發項目中,不可避免地須要解析網絡數據---將服務端下發的JSON數據解析成客戶端可閱讀友好的Model。Objective-C下使用最多的是JSONModel,它能在OC Runtime基礎下很好地完成解析工做。那麼在純Swift代碼中,這個功能是如何實現的?下面開始咱們的探索~前端

  1. 手動解析
  2. 原生:Swift4.0 JSONDecoder
  3. JSONDecoder 問題 及 解決方案

手動解析

假設一個User類要解析,Json以下:git

{
  "userId": 1,
  "name": "Jack",
  "height": 1.7,
}
複製代碼

對應的建立一個User結構體(也能夠是類):github

struct User {
	var userId: Int?
	var name: String?
	var height: CGFloat?
}
複製代碼

把JSON轉成User

在Swift4.0前,咱們以手動解析的方式將JSON model化。給User加一個以JSON爲參數的初始化方法,代碼以下: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。swift

若是某個值不須要強校驗,直接取值再賦值,把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能夠這麼寫:app

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~ 與手動解析不一樣點在於:函數

  1. 移除了手寫init?方法。不須要手動解了工具

  2. User實現Decodable協議,協議的定義以下:spa

    /// 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協議,系統會自動檢測類中的屬性進行初始化工做,省去了人工解析的麻煩~

  3. 使用了JSONDecoder。它是真正的解析工具,主導整個解析過程

讀到這裏,是否是以爲人生從黑暗邁向了光明~~

但是,它並不完美...

JSONDecoder問題及方案

解析JSON常常遇到這樣兩種不一致問題:

  1. 服務端下發的key跟端上不一致。好比,服務端下發key="order_id",端上定義key="orderId"
  2. 服務端下發的日期表達是yyyy-MM-dd HH:mm或者時間戳,但端上是Date類型
  3. 服務端下發的基本類型和端上定義的不一致。服務端下發的是String,端上定義的Int,等

前兩個問題JSONDecoder都能很好地解決。

第一個key不一致問題,JSONDecoder有現成的方案。以上面介紹的例子來講,假設服務端返回的keyuser_id而不是userId,那麼咱們可使用JSONDecoderCodingKeysJSONModel同樣對屬性名稱在加解密時的名稱作轉換。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爲咱們提供了

  1. 解析不一樣key值對象
  2. Date類型可自定義轉換
  3. Float在一些正負無窮及無值得特殊表示。(出現的機率不多,不做具體說明了)

但遇到基本類型端上與服務端不一致時(好比一個數字1,端上的Code是Int型,服務端下發String:"1"),JSONDecoder會拋出typeMismatch異常而終結整個數據的解析。

這讓人有點懊惱,端上的應用,咱們但願它可以儘量穩定,而不是某些狀況下遇到若干個基本類型不一致整個解析就中止,甚至是 Crash。

憂桑

以下面表格所示,咱們但願類型不匹配時,可以這麼處理:左列表明前端的類型,右列表明服務端類型,每一行表明前端類型爲X時,能從服務端下發的哪些類型中轉化,好比String 能夠從 IntorFloat轉化。這幾個類型基本能覆蓋平常服務端下發的數據,其它類型的轉化可根據本身的需求擴充。

前端 服務端
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)
						...循環,直到基本類型
複製代碼

最終的解析落到,_JSONDecoderunboxKeyedDecodingContainerdecoderIfPresent 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致使退出整個解析過程的尷尬。

666

爲什麼不覆蓋decode方法?decodeIfPresent能夠返回Optional值,decode返回肯定類型值。考慮到若是Model內若是定義的類型是No-Optional型,那麼能夠認爲開發者肯定該值必須存在,若是不存在Model極可能是錯誤的,因此直接fail。

完整擴展代碼點我

總結

Swift4.0 JSONDecoder確實爲解析數據帶來了極大的便利。使用方式上相似Objective-C下的JSONModel。但實際開發中仍是須要一些改造才能更好地服務於咱們。

對文章有任何疑問或者有想探討的問題,隨時留言溝通~

相關文章
相關標籤/搜索