Swift Json解析探索

Swift Json解析探索

客戶端開發(fā)項(xiàng)目中,不可避免地需要解析網(wǎng)絡(luò)數(shù)據(jù)---將服務(wù)端下發(fā)的JSON數(shù)據(jù)解析成客戶端可閱讀友好的Model。Objective-C下使用最多的是JSONModel,它能在OC Runtime基礎(chǔ)下很好地完成解析工作。那么在純Swift代碼中,這個(gè)功能是如何實(shí)現(xiàn)的?下面開始我們的探索~

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

手動(dòng)解析

假設(shè)一個(gè)User類要解析,Json如下:

{
  "userId": 1,
  "name": "Jack",
  "height": 1.7,
}

對(duì)應(yīng)的創(chuàng)建一個(gè)User結(jié)構(gòu)體(也可以是類):

struct User {
    var userId: Int?
    var name: String?
    var height: CGFloat?
}

把JSON轉(zhuǎn)成User

在Swift4.0前,我們以手動(dòng)解析的方式將JSON model化。給User加一個(gè)以JSON為參數(shù)的初始化方法,代碼如下:

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所需的具體類型的數(shù)據(jù),填充到具體對(duì)應(yīng)屬性中。如果其中一個(gè)轉(zhuǎn)換失敗或者沒有值,初始化會(huì)失敗返回nil。

如果某個(gè)值不需要強(qiáng)校驗(yàn),直接取值再賦值,把guard let內(nèi)的語句去掉。例如,若height不用校驗(yàn),可看如下代碼:

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發(fā)布,其中一個(gè)重大更新就是JSON的加解密。擺脫手工解析字段的繁瑣,聊聊幾行代碼就可將JSON轉(zhuǎn)換成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~ 與手動(dòng)解析不同點(diǎn)在于:

  1. 移除了手寫init?方法。不需要手動(dòng)解了

  2. User實(shí)現(xiàn)Decodable協(xié)議,協(xié)議的定義如下:

    /// 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協(xié)議只有一個(gè)方法public init(from decoder: Decoder) throws---以Decoder實(shí)例進(jìn)行初始化,初始化失敗可能拋出異常。慶幸的是,只要繼承Decodable協(xié)議,系統(tǒng)會(huì)自動(dòng)檢測(cè)類中的屬性進(jìn)行初始化工作,省去了人工解析的麻煩~

  3. 使用了JSONDecoder。它是真正的解析工具,主導(dǎo)整個(gè)解析過程

讀到這里,是不是覺得人生從黑暗邁向了光明~~

可是,它并不完美...

JSONDecoder問題及方案

解析JSON經(jīng)常遇到這樣兩種不一致問題:

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

前兩個(gè)問題JSONDecoder都能很好地解決。

第一個(gè)key不一致問題,JSONDecoder有現(xiàn)成的方案。以上面介紹的例子來說,假設(shè)服務(wù)端返回的keyuser_id而不是userId,那么我們可以使用JSONDecoderCodingKeysJSONModel一樣對(duì)屬性名稱在加解密時(shí)的名稱做轉(zhuǎn)換。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
    }
}

第二個(gè),Date轉(zhuǎn)換問題。JSONDecoder也為我們提供了單獨(dú)的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
}

設(shè)置好了JSONDecoder屬性dateDecodingStrategy后,解析Date類型就會(huì)按照指定的策略進(jìn)行解析。

類型不一致

至此,JSONDecoder為我們提供了

  1. 解析不同key值對(duì)象
  2. Date類型可自定義轉(zhuǎn)換
  3. Float在一些正負(fù)無窮及無值得特殊表示。(出現(xiàn)的概率很少,不作具體說明了)

但遇到基本類型端上與服務(wù)端不一致時(shí)(比如一個(gè)數(shù)字1,端上的Code是Int型,服務(wù)端下發(fā)String:"1"),JSONDecoder會(huì)拋出typeMismatch異常而終結(jié)整個(gè)數(shù)據(jù)的解析。

這讓人有點(diǎn)懊惱,端上的應(yīng)用,我們希望它能夠盡可能穩(wěn)定,而不是某些情況下遇到若干個(gè)基本類型不一致整個(gè)解析就停止,甚至是 Crash。

憂桑

如下面表格所示,我們希望類型不匹配時(shí),能夠這么處理:左列代表前端的類型,右列代表服務(wù)端類型,每一行代表前端類型為X時(shí),能從服務(wù)端下發(fā)的哪些類型中轉(zhuǎn)化,比如String 可以從 IntorFloat轉(zhuǎn)化。這幾個(gè)類型基本能覆蓋日常服務(wù)端下發(fā)的數(shù)據(jù),其它類型的轉(zhuǎn)化可根據(jù)自己的需求擴(kuò)充。

前端 服務(wù)端
String Int,F(xiàn)loat
Float String
Double String
Bool String, Int

JSONDecoder沒有給我們便利的這種異常處理的API。如何解決呢?最直接的想法,在具體的model內(nèi)實(shí)現(xiàn)init(decoder: Decoder)手動(dòng)解析可以實(shí)現(xiàn),但每個(gè)都這么處理太麻煩。

解決方案:KeyedDecodingContainer方法覆蓋

研究JSONDecoder的源碼,在解析自定義Model過程中,會(huì)發(fā)現(xiàn)這樣一個(gè)調(diào)用關(guān)系。

// 入口方法
JSONDecoder decoder(type:Type data:Data) 
    // 內(nèi)部類,真實(shí)用來解析的
    _JSONDecoder unbox(value:Any type:Type) 
        // Model調(diào)用init方法
        Decodable init(decoder: Decoder) 
            // 自動(dòng)生成的init方法調(diào)用container
            Decoder container(keyedBy:CodingKeys) 
                // 解析的容器
                KeyedDecodingContainer decoderIfPresent(type:Type) or decode(type:Type)
                    // 內(nèi)部類,循環(huán)調(diào)用unbox
                    _JSONDecoder unbox(value:Any type:Type)
                        ...循環(huán),直到基本類型

最終的解析落到,_JSONDecoderunboxKeyedDecodingContainerdecoderIfPresent decode方法。但_JSONDecoder是內(nèi)部類,我們處理不了。最終決定對(duì)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)
    }
}

上述代碼中,第一個(gè)函數(shù)decodeIfPresent(_ type: Int.Type, forKey key: K)是以key的信息解析出Int?值。這里覆蓋了KeyedDecodingContainer中的該函數(shù)的實(shí)現(xiàn),現(xiàn)在已try?的形式以Int類型解析,解析成功則直接返回,失敗則以String類型解析出一個(gè)StringValue,如果解析成功,再把String轉(zhuǎn)換成Int?值。

為什么要寫第二個(gè)函數(shù)呢?

場(chǎng)景:當(dāng)我們Model內(nèi)有其他的非基本類型的Model,比如其他自定義Model,Dictionary<String, Any>,Array<String>等,當(dāng)這些Model 類型不匹配或者出錯(cuò)誤時(shí)也會(huì)拋出異常,導(dǎo)致整個(gè)大Model解析失敗。

覆蓋decodeIfPresent<T>(_ type: T.Type, forKey key: K)可以避免這些場(chǎng)景。至此,當(dāng)類型過程中出現(xiàn)解析的Optional類型出現(xiàn)不匹配時(shí),我們要不是通過轉(zhuǎn)換,要不就是給其賦值nil,避免了系統(tǒng)此時(shí)直接throw exception導(dǎo)致退出整個(gè)解析過程的尷尬。

666

為何不覆蓋decode方法?decodeIfPresent可以返回Optional值,decode返回確定類型值??紤]到如果Model內(nèi)如果定義的類型是No-Optional型,那么可以認(rèn)為開發(fā)者確定該值必須存在,如果不存在Model很可能是錯(cuò)誤的,所以直接fail。

完整擴(kuò)展代碼點(diǎn)我

總結(jié)

Swift4.0 JSONDecoder確實(shí)為解析數(shù)據(jù)帶來了極大的便利。使用方式上類似Objective-C下的JSONModel。但實(shí)際開發(fā)中還是需要一些改造才能更好地服務(wù)于我們。

對(duì)文章有任何疑問或者有想探討的問題,隨時(shí)留言溝通~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,695評(píng)論 19 139
  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫、插件、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 15,667評(píng)論 4 61
  • 1. 創(chuàng)建目錄 要讓集群正常運(yùn)作至少需要三個(gè)主節(jié)點(diǎn), 不過在剛開始試用集群功能時(shí), 強(qiáng)烈建議使用六個(gè)節(jié)點(diǎn): 其中三...
    desirelll閱讀 359評(píng)論 0 0
  • 不知什么時(shí)候開始,讀書的同時(shí)愛上了喝茶。茶,清香撲鼻,給你提神醒腦;書,墨香輕飄,讓你明理益智。在裊裊的茶霧中,打...
    茶雲(yún)澗閱讀 574評(píng)論 0 0
  • 明知是一場(chǎng)意外,你要不要去赴約,或許多年以后,我也沒想到當(dāng)初我會(huì)遇見你,填報(bào)志愿那年我在志愿書上一個(gè)個(gè)學(xué)校徘徊了很...
    z七月上閱讀 295評(píng)論 0 0

友情鏈接更多精彩內(nèi)容