雖然這次 Apple 幫我們做出了 Codable 這個好用的 protocol,但上了戰場之後呢?
如果你已經試過 Swift 4 提供的 Codable protocol,你應該發現 json decode 在 Swift 中已經不像以前那麼不方便了,也不需要再經過 dictionary 的轉換拖慢 decode 速度(像是第三方解析 json 套件:SwiftyJSON)。但 Codable 真的這麼好用嗎?不知道你有沒有注意到,有時候 backend 會因為使用套件的關係,回傳的 json 中會有 {} (空的 json)或者 “”(空字串),這樣會造成 json decode 失敗。
本文專注於在不自己實作 Decodable protocol 中 init(from decoder: Decoder) throws 方法,如果習慣自己實作此 init method 的朋友,將可能不會遇到此問題。
本篇文章將會專注於處理特殊 json 回傳情況,不熟悉 Codable 的朋友,可以參考這篇文章。
情況一:空的 json { }
假設我們有一個 json data 如下,title 是字串,其他兩個 cover 是 BookCover 類別,我們要從 json data 轉換成可以使用的物件,但可以看到 frontCover 回傳的是一個空的 json,這將造成物件轉換失敗。
let jsonData = """ { "title": "Blah", "frontCover": {}, "backCover": { "image": "", "text": "It's good, read it" } } """ struct BookCover: Decodable { var text: String var image: String? enum CodingKeys: String, CodingKey { case text case image } } struct Book: Decodable { var title: String var frontCover: BookCover? var backCover: BookCover? }
失敗的訊息如下:
原因是當我們在轉換 frontCover 時,找不到 title 這個 key 所致。當然,在一個空的 json 中,一定找不到任何 key。
解決方式
因為 frontCover 的型態是 optional 的 BookCover,所以在 decode 時,將會執行 decodeIfPresent
首先要先建立一個 protocol 來處理回傳空 json 的情況,並且對 KeyedDecodingContainer 做一些 extension。
public protocol JSONEmptyRepresentable { // 如果建立物件時會遇到 空 json {},則需要提供自身的 coding keys associatedtype CodingKeyType: CodingKey } extension KeyedDecodingContainer { public func decodeIfPresent(_ type: T.Type, forKey key: K) throws -> T? where T : Decodable & JSONEmptyRepresentable { // 先檢查有沒有我們要找的 frontCover key if contains(key) { // 有的話建立出 nested container // nested container 會根據我們要建立的 type 之中的 coding key type 產生 let container = try nestedContainer(keyedBy: type.CodingKeyType.self, forKey: key) if container.allKeys.isEmpty { // 如果 container 中沒有任何 key,表示我們遇到 {} return nil } } else { // 沒有找到我們要的 key return nil } return try decode(T.self, forKey: key) } }
最後回到類別,並且讓他 conform protocol 即可。
extension BookCover: JSONEmptyRepresentable { typealias CodingKeyType = BookCover.CodingKeys }
情況二:空的字串
類似於上面的狀況,只是空的 json {} 變成空的字串 “”,這個情況下因為 Genre 是 String 資料型態的 enum,所以我們預設有一個 init?(rawValue: String) 的 init method,但如果我們傳入空的字串到 init method 中,就會失敗(回傳 nil)。
public protocol JSONBlankRepresentable: RawRepresentable {} extension KeyedDecodingContainer { public func decodeIfPresent(_ type: T.Type, forKey key: K) throws -> T? where T : Decodable & JSONBlankRepresentable, T.RawValue == String { if contains(key) { if let stringValue = try decodeIfPresent(String.self, forKey: key), stringValue.isEmpty == false { return T.init(rawValue: stringValue) } } return nil } } enum Genre: String, Codable { case thriller case history } extension Genre: JSONBlankRepresentable {} struct Book2: Decodable { var genre: Genre var subgenre: Genre? } if let data = """ { "genre": "thriller", "subgenre": "", } """.data(using: .utf8) { let decoder = JSONDecoder() do { let book = try decoder.decode(Book2.self, from: data) print(book) // "War and Peace: A protocol oriented approach to diplomacy" } catch let e { print(e) } }
結語
以前自己在接 rails 丟的 api 時常常會看到這類情況發生,而當初使用 swiftyJSON 因為只要給一個字串做 subscript 即可拿到想要的值,方便好用且無腦。
但 Codable 這個 protocol 雖然讓解析 json 變得不需要再使用第三方套件,速度應該比轉成 dictionary 更快(沒經過驗證),但也可以看到他對於 json 中的 {}, null, “” 處理上變得更麻煩,如果沒有先確定 backend 回傳型態,一個不小心可能會整個炸掉。
其實這翻譯文章寫到一半我已經心很累,原本想說 Codable 會讓程式變得更乾淨,但現在看起來維護性上其實變得頗差,容錯率也變低,寫起來更麻煩。我想我應該會回去使用慢到炸的 SwiftyJSON 吧?
如果有更好的解法,歡迎留言討論喔!