找出 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。