透過 init(from:) 解析 JSON 裡包裝成字串的 array 或 dictionary

透過 JSONDecoder,我們可以將 JSON 資料變成自訂的型別。不過當 JSON 裡 array 或 dictionary 被包裝成字串時,解析上將麻煩許多。例如以下英國 COVID-19 API 的 JSON。

https://api.covid19uk.live

COVID-19 API 的 JSON 初看似乎滿單純的,第一層是物件,key 是 status & data,data 的資料是 array,array 裡的物件有著 key source,confirmed,death 等。

但麻煩的在後頭。key area 的內容應該是 array,包含英國各地區的人數,但是它卻被包裝成字串,[] 出現在 " " 裡,因此 array 裡的字串還以 \" 做特別處理。

有沒有可能 JSONDecoder 很聰明,明白 area 以字串包裝的內容其實是 array 呢 ? 讓我們試試將 area 的型別宣告成 [Area]。

struct CovidResponse: Codable {
let status: Bool
let data: [CovidData]
}

struct CovidData: Codable {
let confirmed: Int
let area: [Area]
}

struct Area: Codable {
let location: String
let number: Int
}

if let url = URL(string: "https://api.covid19uk.live") {
URLSession.shared.dataTask(with: url) { data, response, error in
if let data {
do {
let covidData = try JSONDecoder().decode(CovidData.self, from: data)
print(covidData.data.first?.confirmed)
print(covidData.data.first?.area.first?.location)
} catch {
print(error)
}
}
}.resume()
}

很遺憾的,我們失敗了,JSONDecoder 沒那麼聰明,它只看出 area 的內容是字串,不明白字串的內容其實是 array。

錯誤訊息如下,提到在解析 area 時 Expected to decode Array<Any> but found a string/data instead。

typeMismatch(Swift.Array<Any>, Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "data", intValue: nil), _JSONKey(stringValue: "Index 0", intValue: 0), CodingKeys(stringValue: "area", intValue: nil)], debugDescription: "Expected to decode Array<Any> but found a string/data instead.", underlyingError: nil))

因此我們得另外寫程式幫忙 JSONDecoder 解析 JSON,程式如下:

struct CovidResponse: Codable {
let status: Bool
let data: [CovidData]
}

struct CovidData: Codable {

let confirmed: Int
let area: [Area]

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
confirmed = try container.decode(Int.self, forKey: .confirmed)
let areaString = try container.decode(String.self, forKey: .area)
if let areaData = areaString.data(using: .utf8),
let area = try? JSONDecoder().decode([Area].self, from: areaData) {
self.area = area
} else {
throw DecodingError.dataCorruptedError(forKey: .area, in: container, debugDescription: "area error")
}
}
}

struct Area: Codable {
let location: String
let number: Int
}

說明

解析的關鍵在於我們須在 struct Data 裡定義 init(from:),自己撰寫將 JSON 資料變成自訂型別的程式,相關說明可參考以下連結。

init(from:) 是型別遵從 protocol Decodable 時需定義的 init,原本我們不用定義是因為 Xcode 的 compiler 很聰明,它能依據我們在自訂型別裡宣告的 property 名稱和型別自動生成 init(from:)的程式。

但現在 area 的內容是字串,我們想將它變成 array,這已經超出 Xcode compiler 的能力範圍,只有靠聰明的人類才能寫出轉換的程式。我們在型別 Data 裡定義的init(from:)如下:

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
confirmed = try container.decode(Int.self, forKey: .confirmed)
let areaString = try container.decode(String.self, forKey: .area)
if let areaData = areaString.data(using: .utf8),
let area = try? JSONDecoder().decode([Area].self, from: areaData) {
self.area = area
} else {
self.area = []
}
}
  • let container = try decoder.container(keyedBy: CodingKeys.self)

從 decoder 呼叫 container(keyedBy:),取得包含 JSON 資料的 container。之後我們可利用 key 讀取 container 的內容。

  • confirmed = try container.decode(Int.self, forKey: .confirmed)

呼叫 decode(_:forKey:),將 key confirmed 對應的內容解析成整數,存入 property confirmed。

function decode(_:forKey:) 是我們手動解析 JSON 的關鍵,透過它的第一個參數指定解析的型別,第二個參數指定 key,如此即可將 JSON 內容一個個讀取出來,變成我們想要的型別。

  • let areaString = try container.decode(String.self, forKey: .area)

key area 的內容是字串,因此我們先將它解析成字串,存入常數 areaString。

if let areaData = areaString.data(using: .utf8), 
let area = try? JSONDecoder().decode([Area].self, from: areaData) {
self.area = area
} else {
self.area = []
}

我們的最終目標是將 areaString 變成 array,存在 Data 的 property area。因此我們先將 areaString 變成 areaData,然後再用 JSONDecoder 將它變成 [Area],若是不幸失敗則讓 area 成為空陣列。

結果

成功將 JSON 解析成 CovidData,原本內容是字串的 area 也變成了 [Area]。

if let url = URL(string: "https://api.covid19uk.live") {
URLSession.shared.dataTask(with: url) { data, response, error in
if let data {
do {
let covidData = try JSONDecoder().decode(CovidData.self, from: data)
print(covidData.data.first?.confirmed ?? 0)

print(covidData.data.first?.area.first?.location ?? "")
} catch {
print(error)
}
}
}.resume()
}

--

--

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

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