利用 CodingKey & init(from:) 將不同的 JSON key 對應到同一個 property

串接 JSON API 時,有時我們會遇到 JSON 的某個 key 名字有多種可能,而我們希望能將它們對應到同一個 property。

比方從 iTunes API 抓資料時,當 media 是 movie,JSON 裡描述資料簡介的欄位是 longDescription。

https://itunes.apple.com/search?term=apple&limit=20&lang=en_us&media=movie

而當 media 是 software 時,JSON 裡描述資料簡介的欄位卻是 description。

此時我們不能使用傳統的方法定義 JSON 對應的 Decodable 型別,因為 key 可能是 longDescription,也可能是 description。

此問題可透過 CodingKey & init(from:) 解決。對 CodingKey & init(from:) 不熟的朋友可先參考以下連結。

了解 CodingKey & init(from:) 後,我們利用它們把不同的 JSON key 對應到同一個 property,範例如下:

定義 JSON 對應的資料型別

struct StoreItem {
let name: String
let artist: String
let kind: String
let description: String
let artworkURL: URL
}
extension StoreItem: Codable {

enum CodingKeys: String, CodingKey {
case name = "trackName"
case artist = "artistName"
case kind
case description
case artworkURL = "artworkUrl100"
}

enum AdditionalKeys: String, CodingKey {
case description = "longDescription"
}

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name)
artist = try container.decode(String.self, forKey: .artist)
kind = try container.decode(String.self, forKey: .kind)
artworkURL = try container.decode(URL.self, forKey: .artworkURL)
if let description = try? container.decode(String.self, forKey: .description) {
self.description = description
} else {
let container = try decoder.container(keyedBy: AdditionalKeys.self)
self.description = (try? container.decode(String.self, forKey: .description)) ?? ""
}
}
}
struct SearchResponse {
let results: [StoreItem]
}
extension SearchResponse: Codable { }

說明:

關鍵在以下這段程式。

if let description = try? container.decode(String.self, forKey: .description) {
self.description = description
} else {
let container = try decoder.container(keyedBy: AdditionalKeys.self)
self.description = (try? container.decode(String.self, forKey: .description)) ?? ""
}
  1. 先以 JSON key description 解碼,若有值則將它存入 property description。
  2. 否則改以 JSON key longDescription 解碼,若有值則將它存入 property description。
  3. 如果以上都失敗,則將 property description 設為空字串。

ps: 剛剛這段程式使用 decode 解碼,若要改用 decodeIfPresent 解碼也可以。

if let description = try? container.decodeIfPresent(String.self, forKey: .description) {
self.description = description
} else {
let container = try decoder.container(keyedBy: AdditionalKeys.self)
self.description = (try? container.decodeIfPresent(String.self, forKey: .description)) ?? ""
}

取得 movie 的資料,JSON 的欄位是 longDescription

  • 新版寫法
let url = URL(string: "https://itunes.apple.com/search?term=swift&media=movie")!
Task {
do {
let (data, _) = try await URLSession.shared.data(from: url)
let searchResponse = try JSONDecoder().decode(SearchResponse.self, from: data)
searchResponse.results.forEach {
print("\($0.name): \($0.description)\n")
}
} catch {
print(error)
}
}
  • 舊版寫法
let url = URL(string: "https://itunes.apple.com/search?term=swift&media=movie")!
URLSession.shared.dataTask(with: url) { data, response, error in
if let data = data {
do {
let searchResponse = try JSONDecoder().decode(SearchResponse.self, from: data)
searchResponse.results.forEach {
print("\($0.name): \($0.description)\n")
}
} catch {
print(error)
}
}
}.resume()

取得 software 的資料,JSON 的欄位是 description

模仿以下寫法,url 裡的 media 改成 software。

let url = URL(string: "https://itunes.apple.com/search?term=swift&media=software")!

參考連結

Develop in Swift Data Collections 2.5 Lab iTunes Search(Part 2)

--

--

彼得潘的 iOS App Neverland
彼得潘的 Swift iOS App 開發問題解答集

彼得潘的iOS App程式設計入門,文組生的iOS App程式設計入門講師,彼得潘的 Swift 程式設計入門,App程式設計入門作者,http://apppeterpan.strikingly.com