利用 JSONDecoder 把 JSON 轉換成自訂型別的資料

抓取網路上的 JSON 資料並不是太困難的事,但是如果想要解析它,或是把它變成方便 App 使用的自訂型別,要如何實現呢 ?

以下我們將介紹如何利用 JSONDecoder 把 JSON 轉換成自訂型別的資料。

接下來讓我們串接一些有趣的 API 試試 JSONDecoder 的威力吧。

歌詞 API

API 的網址如下,title = 後接歌曲的名字。

https://some-random-api.com/others/lyrics?title=how long will i love you

後台回傳的資料如下

{
"title":"How Long Will I Love You",
"author":"Ellie Goulding",
"lyrics":"How long will I love you? As long as stars are above you And longer if I can How long will I need you? As long as the seasons need to Follow their plan How long will I be with you? As long as the sea is bound to Wash up on the sand How long will I want you? As long as you want me to And longer by far How long will I hold you? As long as your father told you As long as you can How long will I give to you? As long as I live through you However long you say How long will I love you? As long as stars are above you And longer if I may How long will I love you? ",
"thumbnail":{
"genius":"https://images.genius.com/498d1108967abafb01a799f6555d0344.300x300x1.png",
},
"links":{
"genius":"https://genius.com/Ellie-goulding-how-long-will-i-love-you-lyrics",
},
"disclaimer":"What the API returns is not guaranteed to return the right lyrics or have a matching title and author.",
}

利用網站 JSON Editor Online 顯示後截圖如下。

定義型別 Song

利用 JSONDecoder,我們可以將網路上抓下來的 JSON 資料從 Data 型別變成自訂型別 Song。至於 Song 是如何定義的,接下來讓我們一步步示範說明吧。

(1) 將 JSON 裡以 { } 描述的 object 變成自訂型別,可以用 class,也可以用 struct 定義,型別名字可自取。

因此我們將下圖第一層 { } 的 object 變成型別 Song。

struct Song {

}

(2) 讓自訂型別遵從 protocol Decodable 或 Codable。

JSONDecoder 可以將資料從 Data 型別變成遵從 protocol Decodable 或 Codable 的自訂型別,Decodable 表示可解碼,Codable 表示可解碼跟編碼。

struct Song: Codable {

}

遵從 protocol Codable 後,Song 將有解碼跟編碼的功能。若是 Song 裡的 property 也是遵從 Codable 的型別,Swift 將自動幫我們定義解碼跟編碼的相關 function。

以下常見的基本型別都遵從 Codable,因此大部份的時候我們都不用自己撰寫解碼編碼的程式。

(3) JSON 裡 object 的 key 將成為自訂型別的 property 名字, 而 property 的型別則由 key 對應的 value 型別決定。

property 可宣告為變數,也可宣告為常數,可依據以下幾點規則決定 property 的型別:

  • value 是網址 ➪ URL。(ps: 如果網址包含 ASCII 以外的文字,請將型別宣告為 String,之後再另外轉成 URL)
  • value 是時間 ➪ Date。
  • value 是整數 ➪ Int
  • value 是浮點數 ➪ Float 或 Double
  • value 是字串 ➪ String
  • value 是 true 或 false ➪ Bool
  • value 是陣列 ➪ Array
  • value 是 object ➪ 遵從 protocol Decodable 或 Codable 的自訂型別。
  • key 不一定會出現或 key 的 value 可能為 null 時 ➪ 型別要宣告為 optional。

ps: 依據以上規則,我們將能處理大部分的 JSON 資料,只有少數 case 要特別處理(比方包含中文的網址)。

因此下圖第一層 { } object 的 key 將成為 Song 的 property。

struct Song: Codable {
let title: String
let author: String
let lyrics: String
}

習慣用 extension 遵從 protocol 的朋友,也可以改用以下寫法。

struct Song {
let title: String
let author: String
let lyrics: String
}

extension Song: Codable { }

值得注意的,property 的名稱和型別不對都會造成轉換失敗。

  • property 名稱和 JSON 裡 object 的 key 名字要一樣。

比方 lyrics 寫成 lyric 會有問題。

struct Song: Codable {
let title: String
let author: String
let lyric: String
}

不過需要的話,我們也有方法讓 property 名稱和 JSON 裡 object 的 key 名字不一樣。等下我們會再介紹。

  • property 型別錯誤將轉換失敗。

property 型別錯誤將轉換失敗,比方 lyrics 是字串,但你卻將型別宣告為 Int。

struct Song: Codable {
let title: String
let author: String
let lyrics: Int
}

值得注意的,只要型別不對就會失敗,就算加問號也一樣,因此 let lyrics: Int? 也會轉換失敗。

(4) 只要定義 App 需要的欄位就好,因此自訂型別的 property 可以少訂,不用將 JSON object 的每個 key 都寫成 property。

比方以下 JSON 第一層的 object 有 6 個 key,title,author,lyrics,thumbnail,links & disclaimer,不過我們的 App 只需要其中的部分資訊,所以我們在 Song 裡只宣告 3 個 property。

struct Song: Codable {
let title: String
let author: String
let lyrics: String
}

(5) 以下兩種情況 property 要宣告為 optional。

(a) JSON object 不一定有的 key 。

(b) JSON object 某個 key 對應的 value 有可能是 null。

如果沒有設為 optional,當 JSON 資料裡找不到跟 property 一樣名字的 key 時,將成造轉換失敗。

測試 JSON 解碼

利用 JSONDecoder 的 function decode 將 Data 型別的 JSON 資料變成遵從型別 Decodable 的資料,若成功將順利印出資料的內容。

  • 方法 1: 使用 async & await
func fetchData() async throws {
guard let url = URL(string: "https://some-random-api.com/others/lyrics?title=how long will i love you") else { return }
let (data, _) = try await URLSession.shared.data(from: url)
let decoder = JSONDecoder()
let song = try decoder.decode(Song.self, from: data)
print(song)
}

Task {
do {
try await fetchData()
} catch {
print(error)
}
}
  • 方法 2: 使用 completion handler
func fetchData() {
if let url = URL(string: "https://some-random-api.com/others/lyrics?title=how long will i love you") {
URLSession.shared.dataTask(with: url) { data, response , error in
if let data {
let decoder = JSONDecoder()
do {
let song = try decoder.decode(Song.self, from: data)
print(song)
} catch {
print(error)
}
} else if let error {
print(error)
}
}.resume()
}
}

fetchData()

ps: iOS 17 的 URL 可以自動對網址進行 URL Encoding,舊版則須手動對網址進行 URL Encoding。

解析 JSON 第二層的 object

JSON 裡可能有不只一層的 object,若想要解析它的內容,記得要為 object 定義對應的 Decodable 型別。換句話說,若 JSON 裡有愈多層的 { },我們可能就要定義愈多的 Decodable 型別。

歌詞 API 回傳的 JSON 裡,thumbnail & links 的內容都是 key 為 genius 的物件,因此我們可為它定義對應的型別 Genius。

struct Song: Codable {
let title: String
let author: String
let lyrics: String
let thumbnail: Genius
let links: Genius
}

struct Genius: Codable {
let genius: URL
}

我們可以分開定義 Song & Genius,也可以在 Song 裡定義 Genius。

struct Song: Codable {
let title: String
let author: String
let lyrics: String
let thumbnail: Genius
let links: Genius

struct Genius: Codable {
let genius: URL
}
}

練習

  • 搭配 AI 建立自己感興趣的 JSON API
  • 更多的隨機 API

iTunes API

了解比較簡單的歌詞 API 例子後,接著讓我們挑戰更複雜的 iTunes API。

iTunes Search API 的文件如下。

我們以從 iTunes 搜尋好聽的暖暖當例子,API 的網址為

https://itunes.apple.com/search?term=暖暖&media=music&country=tw

後台回傳的資料利用網站 JSON Editor Online 顯示後截圖如下:

定義 JSON 對應的型別時,記得由第一層開始層層解析,如此就算它像地獄一樣有十八層我們也可順利解析。

解析第一層

上圖是 JSON 第一層的內容,它是以 { } 描述的物件,因此我們定義它對應的型別 SearchResponse,並且宣告它的兩個 property resultCount & results。

struct SearchResponse: Codable {
let resultCount: Int
let results: []
}

results 的內容是 [ ] 描述的 array,不過我們還不知道 array 成員的型別,所以先寫 [ ],[ ] 裡的內容等下再填。

解析第二層

array 裡的每個成員是 { } 描述的物件,因此我們定義它對應的型別 StoreItem。

struct StoreItem: Codable {

}

然後再回頭將 results 的型別宣告為 [StoreItem]

struct SearchResponse: Codable {
let resultCount: Int
let results: [StoreItem]
}

上圖是 JSON 第二層的 { } 內容,因此我們在 StoreItem 宣告以下 property 對應 { } 裡的 key。

struct StoreItem: Codable {
let trackId: Int
let artistName: String
let trackName: String
let collectionName: String?
let previewUrl: URL
let artworkUrl100: URL
let trackPrice: Double?
let isStreamable: Bool?
}

值得注意的,StoreItem 裡有些 property 被宣告為 optional,因為 JSON 裡有些 key 不會每一筆資料都有,比方不是每首歌都可以買,所以不一定有 trackPrice。因此我們將不一定會有的 collectionName,trackPrice & isStreamable 設為 optional。如果沒有設為 optional,到時候一旦發現沒有這些欄位,將造成轉換失敗。

通常 API 文件會說明哪些欄位不一定會有,所以我們知道哪些 property 要加 ?。若是沒有文件,或是文件寫得不清楚,我們只能一邊測試,一邊針對有問題欄位加上 ?。

現在我們已成功定義出 iTunes 音樂 JSON 對應的型別,答案如下。

struct SearchResponse: Codable {
let resultCount: Int
let results: [StoreItem]
}

struct StoreItem: Codable {
let trackId: Int
let artistName: String
let trackName: String
let collectionName: String?
let previewUrl: URL
let artworkUrl100: URL
let trackPrice: Double?
let isStreamable: Bool?
}

測試 JSON 解碼

  • 方法 1: 使用 async & await
func fetchData() async throws {
guard let url = URL(string: "https://itunes.apple.com/search?term=暖暖&media=music&country=tw") else { return }
let (data, _) = try await URLSession.shared.data(from: url)
let decoder = JSONDecoder()
let searchResponse = try decoder.decode(SearchResponse.self, from: data)
print(searchResponse.results)
}

Task {
do {
try await fetchData()
} catch {
print(error)
}
}
  • 方法 2: 使用 completion handler
func fetchData() {
if let url = URL(string: "https://itunes.apple.com/search?term=暖暖&media=music&country=tw") {
URLSession.shared.dataTask(with: url) { data, response , error in
if let data {
let decoder = JSONDecoder()
do {
let searchResponse = try decoder.decode(SearchResponse.self, from: data)
print(searchResponse.results)
} catch {
print(error)
}
} else if let error {
print(error)
}
}.resume()
}
}

fetchData()

iTunes API 補充

  • 抓取大圖

假設 JSON 的圖片網址如下

https://is3-ssl.mzstatic.com/image/thumb/Music/v4/76/54/ea/7654ea84-2830-26d3-1526-01833cfe68fc/source/100x100bb.jpg

我們可以調整圖片的尺寸,比方將 100x100bb.jpg 改成 500x500bb.jpg。

https://is3-ssl.mzstatic.com/image/thumb/Music/v4/76/54/ea/7654ea84-2830-26d3-1526-01833cfe68fc/source/500x500bb.jpg

Swift 程式寫法。

宣告 computed property artworkUrl500。

struct StoreItem: Codable {
let trackId: Int
let artistName: String
let trackName: String
let collectionName: String?
let previewUrl: URL
let artworkUrl100: URL
let trackPrice: Double?
let isStreamable: Bool?

var artworkUrl500: URL {
artworkUrl100.deletingLastPathComponent().appending(path: "500x500bb.jpg")
}
}

當 JSON 的第一層是 array

掌握剛剛提到的方法,我們已經具備解析大部分 JSON 資料的能力。不過如果 JSON 的第一層是 array,不是 object 呢 ? 比方以下從 GitHub 抓取 twostraws 大大的 followers。

https://api.github.com/users/twostraws/followers

JSON 如下,第一層是 array,array 的成員是 follower 的相關資訊。

這時候我們只有一個小地方要調整,只要在呼叫 decode function 時傳入的型別加上 [ ] 即可,例如以下例子傳入 [Follower].self

struct Follower: Codable {
let login: String
let id: Int
}
  • 方法 1: 使用 async & await

func fetchData() async throws {
guard let url = URL(string: "https://api.github.com/users/twostraws/followers") else { return }
let (data, _) = try await URLSession.shared.data(from: url)
let decoder = JSONDecoder()
let followers = try decoder.decode([Follower].self, from: data)
print(followers)
}

Task {
do {
try await fetchData()
} catch {
print(error)
}
}
  • 方法 2: 使用 completion handler
func fetchData() {
if let url = URL(string: "https://api.github.com/users/twostraws/followers") {
URLSession.shared.dataTask(with: url) { data, response , error in
if let data {
let decoder = JSONDecoder()
do {
let followers = try decoder.decode([Follower].self, from: data)
print(followers)
} catch {
print(error)
}
} else if let error {
print(error)
}
}.resume()
}
}

fetchData()

找出 JSONDecoder 的 decode 錯誤

我們都知道寫程式時常會遇到問題,沒有問題才是不正常的。如果不幸遇到 decode 失敗, 可參考以下連結找出失敗的原因。

讓 property 名稱和 JSON 裡 object 的 key 名字不一樣

解析 JSON 裡的時間

當我們利用 JSONDecoder 串接 API 回傳的 JSON 資料時,通常我們會將時間欄位的資料對應到 Date 型別,方便之後App 使用一些時間運算的 function。不過 API 回傳的時間有許多格式,因此我們須告訴 JSONDecoder 時間的格式,它才能將時間解析後儲存成 Date 型別。

新手的 Swift JSON decode 練習題 & 解答

請 AI 定義 JSON 資料對應的 Codable 型別和指定需要的資料

貼上 JSON,自動產生 Swift Codable 型別的 quicktype

在 Codable 型別裡宣告跟 JSON 資料無關的 property

自訂型別的 property 必須對應 JSON 的 key,不過我們也可以加入跟 JSON 無關的 property 存資料,相關說明可參考以下連結。

iOS 解碼 & 編碼 JSON 的文章整理

關於 iOS 解碼 & 編碼 JSON 的故事,其實還有很多,比方想自己控制解碼 JSON,可定義 function init(from:)。有興趣的朋友可進一步參考以下連結的說明。

--

--

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

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