Swift 程式語言

實測 JSON Decode:Codable Protocol 真的這麼好用嗎?

實測 JSON Decode:Codable Protocol 真的這麼好用嗎?
實測 JSON Decode:Codable Protocol 真的這麼好用嗎?
In: Swift 程式語言
本篇原文(標題:真實世界的 JSON Decode)刊登於作者 Medium,由 David Lin 所著並授權轉載。

雖然這次 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?
}

失敗的訊息如下:

keyNotFound(lldbexpr148.BookCover.CodingKeys.text, Swift.DecodingError.Context(codingPath: [lldbexpr148.Book.(CodingKeys in 26B35E459B7D5969E8B4869C3A09F28B).frontCover], debugDescription: “No value associated with key text (\”text\”).”, underlyingError: nil))

原因是當我們在轉換 frontCover 時,找不到 title 這個 key 所致。當然,在一個空的 json 中,一定找不到任何 key。

解決方式

因為 frontCover 的型態是 optional 的 BookCover,所以在 decode 時,將會執行 decodeIfPresent 來檢查是否為 nil,現在我們要來對有這個 function 的 KeyedDecodingContainer 做一些 extension。

首先要先建立一個 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 吧?

如果有更好的解法,歡迎留言討論喔!

本篇原文(標題:真實世界的 JSON Decode)刊登於作者 Medium,由 David Lin 所著並授權轉載。
作者簡介:David Lin,前 Colorgy iOS 工程師,目前為自由工作者,平時接接案、寫寫文章。為 Swift 愛好者,從 Swift 1.2 就入火坑到現在。目前專注於研究 mobile 架構,從 MVC, MVVM, VIPER, Clean architecture, complex deep link routing 都有研究,想找出適用於 mobile development 皆適用的架構。興趣為:爬山、攝影。曾在 Hahow 上有一線上課程:iOS 入門-從介面設計到開發。Facebook: fb.com/yoxisem544
作者
AppCoda 編輯團隊
此文章為客座或轉載文章,由作者授權刊登,AppCoda編輯團隊編輯。有關文章詳情,請參考文首或文末的簡介。
評論
很好! 你已成功註冊。
歡迎回來! 你已成功登入。
你已成功訂閱 AppCoda 中文版 電子報。
你的連結已失效。
成功! 請檢查你的電子郵件以獲取用於登入的連結。
好! 你的付費資料已更新。
你的付費方式並未更新。