Codable 初看像魔法。在 struct 加上: Codable五個字母,JSON 轉換就免費出現了。但接上實際 API 後,魔法便消失:伺服器使用 snake_case,Swift 使用 camelCase;日期格式依 API 而異;清單中只要有一筆異常,整個解碼就會失敗,畫面也會整片空白。
中級系列第 6 篇介紹 Codable 深入應用,從自動合成實際產生什麼,到實務必遇到的四種情境(鍵名、日期、巢狀、部分失敗)的標準解法,一次整理。
魔法的真相 — 編譯器替你撰寫的程式碼
Codable 是Encodable & Decodable的 typealias,分別要求「如何將自己寫入編碼器」與「如何從解碼器建立自己」的協定。看似魔法,其實是編譯器自動合成(synthesis)的結果。只要所有儲存屬性都符合 Codable,編譯器就會替你產生兩項內容。
第一是 CodingKeys enum:以屬性名稱作為 case 的鍵名清單。第二是透過這些鍵逐一讀寫屬性的init(from:)與encode(to:)實作。也就是說,Codable 客製化就是決定要手動取代哪一項產物。只有鍵名不同,就寫 CodingKeys;結構本身不同,則連 init(from:) 都要實作。掌握這個框架後,後續內容都能整理成「要手寫到什麼程度」。
另一個重要事實:Codable 並非 JSON 專用。由於編碼器與解碼器可以替換,你可以用 PropertyListEncoder 處理 plist,也能透過第三方套件處理 XML 或 YAML。型別只需要知道「如何表示自己」,格式則由編碼器決定,這就是關注點分離。
情境 1. 鍵名不同 — CodingKeys 與鍵名策略
伺服器提供user_name,但你想將屬性命名為 userName 時,處方分成兩個層次。
若全域規則一致,只需設定解碼器的一行:decoder.keyDecodingStrategy = .convertFromSnakeCase。所有鍵都會從 snake_case 自動轉成 camelCase。若整個 API 都遵循這項慣例,這就是正解。
若規則不一致,或想直接改名,就手動撰寫 CodingKeys。
struct User: Codable {
let userName: String
let signupDate: Date
enum CodingKeys: String, CodingKey {
case userName = "user_nm" // 伺服器的舊版鍵名
case signupDate = "created"
}
}
還有一個副作用要記住:從 CodingKeys 移除 case,該屬性就會被排除在編碼與解碼之外。當你想在回應模型中放入快取旗標等僅限本機的狀態時,可以使用這項技巧;但被排除的屬性必須有預設值。
情境 2. 日期 — 格式的地雷區
Date 是 Codable 實務中最常出問題的地雷。JSON 沒有統一的日期標準,因此各伺服器可能使用 Unix 時間戳記(1720000000)、ISO 8601(“2026-07-15T09:30:00Z”)或自訂格式(“2026-07-15 09:30”)。
處方是將解碼器的 dateDecodingStrategy 設為符合伺服器格式:.secondsSince1970、.iso8601,自訂格式則使用.formatted(formatter)。使用自訂 DateFormatter 有兩個陷阱。locale 若不固定為en_US_POSIX,解析可能受使用者 12/24 小時設定影響而失敗。此外,伺服器傳回含毫秒的 ISO 8601 時,預設的.iso8601會失敗;開啟 ISO8601DateFormatter 的毫秒選項即可解決。「只有特定使用者日期解析失敗」這類神秘錯誤,八成就是這兩個陷阱。
若同一個 API 中每個欄位的格式都不同,實際做法是只將該欄位以 String 接收,再透過計算屬性轉換,或使用.custom策略分支處理。
情境 3. 巢狀與結構不一致 — 伺服器的形狀 vs 我的形狀
這是伺服器 JSON 深度包裝的情況,例如{"data": {"user": {...}}}。最簡單的處方是照原樣建立外層型別:用struct Envelope: Codable { let data: DataBox }鏡像伺服器結構,再在呼叫端透過envelope.data.user取出內容。明確又容易偵錯,因此實務上以此作為基本做法較好。
如果想讓模型形狀不同於伺服器,例如使用扁平的 User,就手動實作 init(from:),透過 nestedContainer 深入階層。程式碼會增加,但模型會更乾淨、更以領域為中心。判斷標準是模型的使用範圍:全 App 使用的核心模型值得手寫 init(from:);只供單一畫面的回應,鏡像外層結構更經濟。
情境 4. 部分失敗 — 因為一筆資料讓全部失敗
這是實務中最痛苦的情況。100 筆商品的清單中,只要一筆必要欄位是 null,整個陣列解碼就會 throw,畫面也會空白。就像錯誤處理篇提到的,拋出的錯誤會向上傳播。
第一道防線是 Optional。伺服器可能省略的欄位,就誠實宣告為let thumbnail: URL?。把「可能不存在」刻進型別的 Optional 原則,同樣適用於模型設計。
結構上的防禦是容許失敗的包裝器。建立泛型包裝器,將元素解碼失敗吞成 nil,是實務上常見的標準模式。
struct FailableItem<T: Decodable>: Decodable {
let value: T?
init(from decoder: Decoder) throws {
value = try? T(from: decoder) // 失敗時變成 nil nil
}
}
let items = try decoder.decode([FailableItem<Product>].self, from: data)
.compactMap(\.value) // 只保留成功的項目
這就是 try? 與 compactMap 這兩個前文工具的組合。它把「丟掉一筆壞資料,保留其餘資料」的政策表達成一個型別。但要注意,這個模式會安靜地吞掉失敗。因此在正式環境應記錄失敗數量,避免伺服器資料問題被掩蓋。
偵錯 — DecodingError 早已知道答案
最後介紹解碼失敗時的調查方法。catch try decoder.decode(...)後會捕捉 DecodingError,而這個錯誤比想像中更有幫助。四種情況(keyNotFound、typeMismatch、valueNotFound、dataCorrupted)都包含是哪個鍵、位於哪條 codingPath、原本期待什麼以及實際收到什麼。
因此解碼失敗時,請養成檢查print(error)而不是print(error as? DecodingError)的習慣;至少在開發期間,應在 catch 區塊中依情況印出 codingPath。這能大幅縮短偵錯時間。「JSON 無法解析」的大多數答案,其實已經寫在錯誤裡。
總結
- Codable 的魔法就是編譯器合成。它替你撰寫 CodingKeys enum 與 init(from:)/encode(to:);客製化的問題,只在於要手動取代其中多少內容。
- 鍵名:一致的 snake_case 使用一行 keyDecodingStrategy,不規則時手動宣告 CodingKeys。移除 case 就會將該欄位排除在轉換之外。
- 日期:明確指定符合伺服器格式的 dateDecodingStrategy,並小心 en_US_POSIX locale 與含毫秒 ISO 8601 的陷阱。
- 巢狀:預設鏡像外層結構,只有核心模型才用 init(from:) + nestedContainer 攤平。
- 部分失敗:誠實宣告 Optional + FailableItem 模式(try? + compactMap),並透過日誌讓被吞掉的失敗可見。
- 偵錯:答案就在 DecodingError 的 codingPath 中。
下一篇介紹 @State、@Published 等 at-sign 語法的原理,以及屬性包裝器。實作屬性篇預告的「包住儲存屬性的語法」。

![[Swift 中級 #6] Swift Codable 深入解析,4 大地雷處方箋 封面圖](/assets/images/posts/c82f52ea-3b48-40d5-8739-a94bd38731cf/swift-codable-deep-dive-1.jpg)