找出 JSONDecoder 的 decode 錯誤

利用 JSONDecoder,我們可以很方便地將 API 回傳的 JSON 資料轉成遵從 Decodable 的自訂型別,例如以下結合 Dog API,抓取可愛小狗圖片的例子。

struct Dog: Decodable {
var status: String
var message: URL
}
  • 寫法 1: 使用 async & await。
func fetch() async throws {
if let url = URL(string: "https://dog.ceo/api/breeds/image/random") {
let (data, _) = try await URLSession.shared .data(from: url)
let dog = try JSONDecoder().decode(Dog.self, from: data)
print(dog)
}
}

Task {
do {
try await fetch()
} catch {
print(error)
}
}
  • 寫法 2: 使用 completion handler。
func fetch() {
if let url = URL(string: "https://dog.ceo/api/breeds/image/random") {
URLSession.shared.dataTask(with: url) { data, response, error in
if let data {
do {
let dog = try JSONDecoder().decode(Dog.self, from: data)
print(dog)
} catch {
print(error)
}
}
}.resume()
}
}

fetch()

結果

此方法的關鍵在於遵從 Decodable 的自訂型別和 JSON 資料對應,如果不小心犯了某些小錯誤,比方把 message 拼成 massage,將讓 JSONDecoder 的 decode 解碼失敗。不過這種小錯誤有時很難發現,所謂旁觀者清,不如讓 Swift 主動告訴我們錯在哪吧。

在研究解碼錯誤前,最好先將抓到的 data 變成字串印出,檢查得到的資料是否正確。有時用 PostMan 可以抓到資料,不代表程式可以抓到,而且程式抓到的資料也可能跟 PostMan 不一樣。因此有問題時可在程式印出 JSON,檢查抓到的資料長什麼樣子

印出 JSON,檢查 JSON 的內容

  • 寫法 1: 使用 async & await。
func fetch() async throws {
if let url = URL(string: "https://dog.ceo/api/breeds/image/random") {
let (data, _) = try await URLSession.shared .data(from: url)
if let content = String(data: data, encoding: .utf8) {
print(content)
}
}
}
  • 寫法 2: 使用 completion handler。
func fetch() {
if let url = URL(string: "https://dog.ceo/api/breeds/image/random") {
URLSession.shared.dataTask(with: url) { data, response, error in
if let data,
let content = String(data: data, encoding: .utf8) {
print(content)
}
}.resume()
}
}

確定抓到的 JSON 資料沒問題後,我們有以下三種找出解碼錯誤的方法 :

解法1: 利用 do catch 搭配 try

  • 寫法 1: 使用 async & await。
func fetch() async throws {
if let url = URL(string: "https://dog.ceo/api/breeds/image/random") {
let (data, _) = try await URLSession.shared .data(from: url)
let dog = try JSONDecoder().decode(Dog.self, from: data)
print(dog)
}
}

Task {
do {
try await fetch()
} catch {
print(error)
}
}
  • 寫法 2: 使用 completion handler。
func fetch() {
if let url = URL(string: "https://dog.ceo/api/breeds/image/random") {
URLSession.shared.dataTask(with: url) { data, response, error in
if let data {
do {
let dog = try JSONDecoder().decode(Dog.self, from: data)
print(dog)
} catch {
print(error)
}
}
}.resume()
}
}

fetch()

當 try 後呼叫的 function 丟出錯誤時,我們可在 catch 的 { } 裡利用自動生成的常數 error 得到錯誤資訊。接下來就讓我們故意犯錯,親眼見證幾種常見的錯誤吧。

1 property 的名字拼錯,和 JSON dictionary 的 key 不一樣

比方 JSON 裡是 message,我們拼成 massage。

struct Dog: Decodable {
var status: String
var massage: URL
}

錯誤訊息 keyNotFound。

keyNotFound(CodingKeys(stringValue: "massage", intValue: nil), Swift.DecodingError.Context(codingPath: [], debugDescription: "No value associated with key CodingKeys(stringValue: \"massage\", intValue: nil) (\"massage\").", underlyingError: nil))

keyNotFound 表示 JSONDecoder 在 JSON 資料裡找不到某個 key 對應的內容,CodingKeys 後的文字說明找不到的 key 叫做 massage,因此造成問題的凶手是 property massage。

2 property 的型別不對。

比方 status 應該是 String,我們卻宣告成 Int。

struct Dog: Decodable {
var status: Int
var message: URL
}

錯誤訊息 typeMismatch。

typeMismatch(Swift.Int, Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "status", intValue: nil)], debugDescription: "Expected to decode Int but found a string/data instead.", underlyingError: nil))

typeMismatch 表示 property 的型別有問題,CodingKeys 後的文字說明有問題的 property 是 status,而 debugDescription 更進一步說明型別不合的原因,Expected to decode Int but found a string/data instead,原來我們宣告成 Int,但 JSON 裡的資料是 String。

3 JSON 某些 key 不一定存在,宣告成 property 時沒有宣告為 Optional

JSON 裡某些 key 不一定存在, 它們宣告成 property 時必須是 optional,否則找不到 key 時將出現 keyNotFound 錯誤。

範例

let data = """
[
{
"name": "最高學以致用法",
"author": "樺澤紫苑",
"price": 370
},
{
"name": "銀河系邊緣的小異常",
"author": "艾加.凱磊"
}
]
""".data(using: .utf8)!
struct Book: Decodable {
let name: String
let author: String
let price: Int
}
let decoder = JSONDecoder()
do {
let books = try decoder.decode([Book].self, from: data)
print(books)
} catch {
print(error)
}

銀河系邊緣的小異常沒有 price,因此 price 找不到造成錯誤,出現錯誤訊息 keyNotFound。

keyNotFound(CodingKeys(stringValue: "price", intValue: nil), Swift.DecodingError.Context(codingPath: [_JSONKey(stringValue: "Index 1", intValue: 1)], debugDescription: "No value associated with key CodingKeys(stringValue: \"price\", intValue: nil) (\"price\").", underlyingError: nil))

值得注意的,當有問題的資料是 array 裡的某一筆資料時,我們還可以從 印出的 error 找出它是第幾筆。比方剛剛的錯誤訊息提到 Index 1,表示它是第二筆資料。

4 JSON 裡 key 的 value 可能是 null,宣告成 property 時沒有宣告為 Optional

當 JSON 裡 key 的 value 可能是 null 時,宣告的 property 必須是 optional,否則找不到 value 時將出現 valueNotFound 錯誤。

比方 JSON 如下,第二本書的 price 是 null。

let data = """
[
{
"name": "最高學以致用法",
"author": "樺澤紫苑",
"price": 370
},
{
"name": "銀河系邊緣的小異常",
"author": "艾加.凱磊",
"price": null
}
]
""".data(using: .utf8)!

JSON 對應的 Book 定義如下。

struct Book: Decodable {
let name: String
let author: String
let price: Int
}

price 應該要是 optional,若是忘了加問號,錯誤訊息將顯示 valueNotFound。

valueNotFound(Swift.Int, Swift.DecodingError.Context(codingPath: [_JSONKey(stringValue: "Index 1", intValue: 1), CodingKeys(stringValue: "price", intValue: nil)], debugDescription: "Expected Int value but found null instead.", underlyingError: nil))

5 URL 有問題 (ps: 此問題在 iOS 17 已修正)

JSON 裡包含的網址可能包含特別字元,在 iOS 17 之前的版本需要特別處理才能轉成 URL,比方以下例子第二個 link 的網址包含中文。

let data = """
[
{"name": "SwiftUI - How to setup a project",
"link": "https://medium.com/flawless-app-stories/swiftui-getting-started-372389fff423"},
{ "name": "iOS SDK 選東西的 view controller & delegate 範例",
"link": "https://medium.com/彼得潘的-swift-ios-app-開發問題解答集/ios-sdk-選東西的-view-controller-delegate-範例-c3f0b5238933"}
]
""".data(using: .utf8)!

struct Webpage: Decodable {
let name: String
let link: URL
}

let decoder = JSONDecoder()
do {
let webpages = try decoder.decode([Webpage].self, from: data)
print(webpages)
} catch {
print(error)
}

印出的錯誤訊息將告訴我們 Invalid URL string。

dataCorrupted(Swift.DecodingError.Context(codingPath: [_JSONKey(stringValue: "Index 1", intValue: 1), CodingKeys(stringValue: "link", intValue: nil)], debugDescription: "Invalid URL string.", underlyingError: nil))

此問題的解法是將 property 的型別宣告成 String,然後再自己將它轉成 URL。因此剛剛的範例可以改成以下寫法:

struct Webpage: Decodable {
let name: String
let link: String
var url: URL? {
link.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed).flatMap { URL(string: $0) }
}
}

let decoder = JSONDecoder()
do {
let webpages = try decoder.decode([Webpage].self, from: data)
print(webpages)
} catch {
print(error)
}

利用 catch 辨別每一種錯誤

若想辨別每一種解析 JSON 的錯誤,我們也可以在 catch 後搭配 DecodingError 的各種 case。

let decoder = JSONDecoder()
do {
let dog = try decoder.decode(Dog.self, from: data)
print(dog)
} catch DecodingError.keyNotFound(let key, let context) {
print("keyNotFound", key, context)
} catch DecodingError.typeMismatch(let type, let context) {
print("typeMismatch", type, context)
} catch DecodingError.valueNotFound(let value, let context) {
print("valueNotFound", value, context)
} catch DecodingError.dataCorrupted(let context) {
print("dataCorrupted", context)
} catch {
print(error)
}

解法2: 使用 try? 時,利用 Swift Error Breakpoint 找錯誤

解法3: 利用 try!

  • 寫法 1: 使用 async & await。
struct Dog: Decodable {
var status: String
var massage: URL
}

func fetch() async throws {
if let url = URL(string: "https://dog.ceo/api/breeds/image/random") {
let (data, _) = try await URLSession.shared .data(from: url)
let dog = try JSONDecoder().decode(Dog.self, from: data)
print(dog)
}
}

Task {
try! await fetch()
}
  • 寫法 2: 使用 completion handler。
struct Dog: Decodable {
var status: String
var massage: URL
}

func fetch() {
if let url = URL(string: "https://dog.ceo/api/breeds/image/random") {
URLSession.shared.dataTask(with: url) { data, response, error in
if let data {
let dog = try! JSONDecoder().decode(Dog.self, from: data)
print(dog)
}
}.resume()
}
}

fetch()

利用 try!,當 JSON 解析失敗時,程式將馬上閃退,然後在 Console 好心地告訴我們閃退的原因。

Fatal error: 'try!' expression unexpectedly raised an error: Swift.DecodingError.keyNotFound(CodingKeys(stringValue: "massage", intValue: nil), Swift.DecodingError.Context(codingPath: [], debugDescription: "No value associated with key CodingKeys(stringValue: \"massage\", intValue: nil) (\"massage\").", underlyingError: nil)): file MyPlayground.playground, line 11

不過 try! 的寫法比較危險,它會讓程式閃退,因此修正問題後,最好再改用 do catch 或 try? 的寫法解析 JSON。

--

--

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

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