axiom-codable by charleswiltgen/axiom
npx skills add https://github.com/charleswiltgen/axiom --skill axiom-codableSwift 6.x 中用于 JSON 和 PropertyList 编码/解码的 Codable 协议一致性综合指南。
Has your type...
├─ 所有属性都符合 Codable? → 自动合成(只需添加 `: Codable`)
├─ 属性名与 JSON 键不同? → 自定义 CodingKeys
├─ 需要排除某些属性? → 自定义 CodingKeys
├─ 带有关联值的枚举? → 检查枚举合成模式
├─ 需要结构转换? → 手动实现 + 桥接类型
├─ 需要 JSON 中没有的数据? → DecodableWithConfiguration (iOS 15+)
└─ 复杂的嵌套 JSON? → 手动实现 + 嵌套容器
| 错误 | 解决方案 |
|---|---|
| "Type 'X' does not conform to protocol 'Decodable'" | 确保所有存储属性都符合 Codable |
| "No value associated with key X" | 检查 CodingKeys 是否与 JSON 键匹配 |
| "Expected to decode X but found Y instead" | 类型不匹配;检查 JSON 结构或使用桥接类型 |
| "keyNotFound" | JSON 缺少预期的键;将属性设为可选或提供默认值 |
| "Date parsing failed" | 在解码器上配置 dateDecodingStrategy |
当所有存储属性都符合 Codable 时,Swift 会自动合成 Codable 一致性。
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
// ✅ 自动合成
struct User: Codable {
let id: UUID // Codable
var name: String // Codable
var membershipPoints: Int // Codable
}
// JSON: {"id":"...", "name":"Alice", "membershipPoints":100}
要求:
enum Direction: String, Codable {
case north, south, east, west
}
// 编码为: "north"
原始值本身成为 JSON 表示形式。
enum Status: Codable {
case success
case failure
case pending
}
// 编码为: {"success":{}}
每个 case 成为一个对象,其中 case 名称作为键,空字典作为值。
enum APIResult: Codable {
case success(data: String, count: Int)
case error(code: Int, message: String)
}
// success case 编码为:
// {"success":{"data":"example","count":5}}
注意:未标记的关联值会生成 _0、_1 键:
enum Command: Codable {
case store(String, Int) // ❌ 未标记
}
// 编码为: {"store":{"_0":"value","_1":42}}
修复:始终标记关联值以获得可预测的 JSON:
enum Command: Codable {
case store(key: String, value: Int) // ✅ 已标记
}
// 编码为: {"store":{"key":"value","value":42}}
自动合成在以下情况下失败:
@Published、@State(使用 Codable 类型的 @AppStorage 除外)init(from:)使用 CodingKeys 枚举来自定义编码/解码,无需完整的手动实现。
struct Article: Codable {
let url: URL
let title: String
let body: String
enum CodingKeys: String, CodingKey {
case url = "source_link" // JSON 使用 "source_link"
case title = "content_name" // JSON 使用 "content_name"
case body // 与 JSON 键匹配
}
}
// JSON: {"source_link":"...", "content_name":"...", "body":"..."}
从 CodingKeys 中省略属性以将其排除在编码/解码之外:
struct NoteCollection: Codable {
let name: String
let notes: [Note]
var localDrafts: [Note] = [] // ✅ 必须有默认值
enum CodingKeys: CodingKey {
case name
case notes
// localDrafts 被省略 - 不进行编码/解码
}
}
规则:被排除的属性需要默认值,否则必须手动实现 init(from:)。
用于一致的 snake_case → camelCase 转换:
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
// JSON: {"first_name":"Alice", "last_name":"Smith"}
// 解码为: User(firstName: "Alice", lastName: "Smith")
使用 {CaseName}CodingKeys 自定义枚举关联值的键:
enum Command: Codable {
case store(key: String, value: Int)
case delete(key: String)
enum StoreCodingKeys: String, CodingKey {
case key = "identifier" // 将 "key" 重命名为 "identifier"
case value = "data" // 将 "value" 重命名为 "data"
}
enum DeleteCodingKeys: String, CodingKey {
case key = "identifier"
}
}
// store case 编码为: {"store":{"identifier":"x","data":42}}
模式:{CaseName}CodingKeys 使用大写的 case 名称。
对于 JSON 和 Swift 模型之间的结构差异,实现 init(from:) 和 encode(to:)。
| 容器 | 何时使用 |
|---|---|
| Keyed | 具有字符串键的类字典数据 |
| Unkeyed | 类数组的顺序数据 |
| Single-value | 编码为单个值的包装器类型 |
| Nested | 分层 JSON 结构 |
展平分层 JSON:
// JSON:
// {
// "latitude": 37.7749,
// "longitude": -122.4194,
// "additionalInfo": {
// "elevation": 52
// }
// }
struct Coordinate {
var latitude: Double
var longitude: Double
var elevation: Double // 在 JSON 中嵌套,在 Swift 中扁平
enum CodingKeys: String, CodingKey {
case latitude, longitude, additionalInfo
}
enum AdditionalInfoKeys: String, CodingKey {
case elevation
}
}
extension Coordinate: Decodable {
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
latitude = try values.decode(Double.self, forKey: .latitude)
longitude = try values.decode(Double.self, forKey: .longitude)
let additionalInfo = try values.nestedContainer(
keyedBy: AdditionalInfoKeys.self,
forKey: .additionalInfo
)
elevation = try additionalInfo.decode(Double.self, forKey: .elevation)
}
}
extension Coordinate: Encodable {
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(latitude, forKey: .latitude)
try container.encode(longitude, forKey: .longitude)
var additionalInfo = container.nestedContainer(
keyedBy: AdditionalInfoKeys.self,
forKey: .additionalInfo
)
try additionalInfo.encode(elevation, forKey: .elevation)
}
}
当 JSON 结构与 Swift 模型根本不同时:
// JSON: {"USD": 1.0, "EUR": 0.85, "GBP": 0.73}
// 期望: [ExchangeRate]
struct ExchangeRate {
let currency: String
let rate: Double
}
// 用于解码的桥接类型
private extension ExchangeRate {
struct List: Decodable {
let values: [ExchangeRate]
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let dictionary = try container.decode([String: Double].self)
values = dictionary.map { ExchangeRate(currency: $0, rate: $1) }
}
}
}
// 公共接口
extension ExchangeRate {
static func decode(from data: Data) throws -> [ExchangeRate] {
let list = try JSONDecoder().decode(List.self, from: data)
return list.values
}
}
let decoder = JSONDecoder()
// 1. ISO 8601 (推荐)
decoder.dateDecodingStrategy = .iso8601
// 期望: "2024-02-15T17:00:00+01:00"
// 2. Unix 时间戳 (秒)
decoder.dateDecodingStrategy = .secondsSince1970
// 期望: 1708012800
// 3. Unix 时间戳 (毫秒)
decoder.dateDecodingStrategy = .millisecondsSince1970
// 期望: 1708012800000
// 4. 自定义格式化器
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
formatter.locale = Locale(identifier: "en_US_POSIX") // ✅ 始终设置
formatter.timeZone = TimeZone(secondsFromGMT: 0) // ✅ 始终设置
decoder.dateDecodingStrategy = .formatted(formatter)
// 5. 自定义闭包
decoder.dateDecodingStrategy = .custom { decoder in
let container = try decoder.singleValueContainer()
let dateString = try container.decode(String.self)
if let date = ISO8601DateFormatter().date(from: dateString) {
return date
}
throw DecodingError.dataCorruptedError(
in: container,
debugDescription: "Cannot decode date string \(dateString)"
)
}
默认:2024-02-15T17:00:00+01:00 时区必需:没有时区偏移,跨区域解码可能会失败
// ❌ 无时区 - 解析取决于设备区域设置
"2024-02-15T17:00:00"
// ✅ 带时区 - 明确无误
"2024-02-15T17:00:00+01:00"
自定义闭包为每个日期运行 - 优化昂贵的操作:
// ❌ 为每个日期创建新的格式化器
decoder.dateDecodingStrategy = .custom { decoder in
let formatter = DateFormatter() // 昂贵!
// ...
}
// ✅ 重用格式化器
let sharedFormatter = DateFormatter()
sharedFormatter.dateFormat = "yyyy-MM-dd"
decoder.dateDecodingStrategy = .custom { decoder in
// 使用 sharedFormatter
}
处理将数字编码为字符串的 API:
protocol StringRepresentable: CustomStringConvertible {
init?(_ string: String)
}
extension Int: StringRepresentable {}
extension Double: StringRepresentable {}
struct StringBacked<Value: StringRepresentable>: Codable {
var value: Value
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let string = try container.decode(String.self)
guard let value = Value(string) else {
throw DecodingError.dataCorruptedError(
in: container,
debugDescription: "Cannot convert '\(string)' to \(Value.self)"
)
}
self.value = value
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(value.description)
}
}
// 用法
struct Product: Codable {
let name: String
private let _price: StringBacked<Double>
var price: Double {
get { _price.value }
set { _price = StringBacked(value: newValue) }
}
enum CodingKeys: String, CodingKey {
case name
case _price = "price"
}
}
// JSON: {"name":"Widget","price":"19.99"}
// 解码为: Product(name: "Widget", price: 19.99)
用于可能返回不同类型的松散类型 API:
struct FlexibleValue: Codable {
let stringValue: String
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let string = try? container.decode(String.self) {
stringValue = string
} else if let int = try? container.decode(Int.self) {
stringValue = String(int)
} else if let double = try? container.decode(Double.self) {
stringValue = String(double)
} else {
throw DecodingError.dataCorruptedError(
in: container,
debugDescription: "Cannot decode value to String, Int, or Double"
)
}
}
}
警告:除非 API 确实不可预测,否则避免使用此模式。优先使用严格类型。
用于需要 JSON 中不可用数据的类型:
struct User: Encodable, DecodableWithConfiguration {
let id: UUID
var name: String
var favorites: Favorites // 不在 JSON 中,通过配置注入
enum CodingKeys: CodingKey {
case id, name
}
init(from decoder: Decoder, configuration: Favorites) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(UUID.self, forKey: .id)
name = try container.decode(String.self, forKey: .name)
favorites = configuration // 注入
}
}
// 用法 (iOS 17+)
let favorites = try await fetchFavorites()
let user = try JSONDecoder().decode(
User.self,
from: data,
configuration: favorites
)
extension JSONDecoder {
private struct ConfigurationDecodingWrapper<T: DecodableWithConfiguration>: Decodable {
var wrapped: T
init(from decoder: Decoder) throws {
let config = decoder.userInfo[configurationUserInfoKey] as! T.DecodingConfiguration
wrapped = try T(from: decoder, configuration: config)
}
}
func decode<T: DecodableWithConfiguration>(
_ type: T.Type,
from data: Data,
configuration: T.DecodingConfiguration
) throws -> T {
let decoder = JSONDecoder()
decoder.userInfo[Self.configurationUserInfoKey] = configuration
let wrapper = try decoder.decode(ConfigurationDecodingWrapper<T>.self, from: data)
return wrapper.wrapped
}
}
private let configurationUserInfoKey = CodingUserInfoKey(rawValue: "configuration")!
仅解码需要的字段:
struct ArticlePreview: Decodable {
let id: UUID
let title: String
// 省略 body、comments 等
}
// JSON 有许多字段,但我们只解码 id 和 title
do {
let user = try decoder.decode(User.self, from: data)
} catch DecodingError.keyNotFound(let key, let context) {
print("Missing key '\(key)' at path: \(context.codingPath)")
} catch DecodingError.typeMismatch(let type, let context) {
print("Type mismatch for \(type) at path: \(context.codingPath)")
} catch DecodingError.valueNotFound(let type, let context) {
print("Value not found for \(type) at path: \(context.codingPath)")
} catch DecodingError.dataCorrupted(let context) {
print("Data corrupted at path: \(context.codingPath)")
} catch {
print("Other error: \(error)")
}
1. 美化打印 JSON
let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
let jsonData = try encoder.encode(user)
print(String(data: jsonData, encoding: .utf8)!)
2. 检查编码路径
// 在自定义 init(from:) 中
print("Decoding at path: \(decoder.codingPath)")
3. 验证 JSON 结构
// 快速检查:能否解码为 Any?
let json = try JSONSerialization.jsonObject(with: data)
print(json) // 查看实际结构
| 反模式 | 成本 | 更好的方法 |
|---|---|---|
| 手动构建 JSON 字符串 | 注入漏洞、转义错误、无类型安全 | 使用 JSONEncoder |
try? 吞没 DecodingError | 静默失败、调试噩梦、数据丢失 | 处理特定的错误情况 |
| 使用可选属性避免解码错误 | 运行时崩溃、到处都需要 nil 检查、掩盖结构问题 | 修复 JSON/模型不匹配或使用 DecodableWithConfiguration |
| 复制部分模型 | 每次更改需要 2-5 小时维护、同步问题、脆弱 | 使用桥接类型或配置 |
| 忽略日期时区 | 跨区域间歇性错误、数据损坏 | 始终使用带时区的 ISO8601 或显式 UTC |
对 Codable 类型使用 JSONSerialization | 样板代码多 3 倍、手动类型转换、容易出错 | 使用 JSONDecoder/JSONEncoder |
| DateFormatter 没有区域设置 | 在非美国区域设置中解析失败 | 设置 locale = Locale(identifier: "en_US_POSIX") |
// ❌ 静默失败 - 等待发生的生产环境错误
let user = try? JSONDecoder().decode(User.self, from: data)
// 如果失败,user 为 nil - 为什么?不知道。
// ✅ 显式错误处理
do {
let user = try JSONDecoder().decode(User.self, from: data)
} catch {
logger.error("Failed to decode user: \(error)")
// 现在你知道为什么失败了
}
上下文:API 集成明天截止,解码器在某些边缘情况下失败。
压力:"我们稍后可以调试,现在先让它工作。"
你会这样合理化的原因:
实际会发生的情况:
纪律性回应:
"在这里使用
try?意味着我们会静默丢失数据。让我花 5 分钟处理特定的错误情况。如果确实很罕见,我会记录下来,以便我们可以修复根本原因。"
5 分钟修复:
do {
return try decoder.decode(User.self, from: data)
} catch DecodingError.keyNotFound(let key, let context) {
logger.error("Missing key '\(key)' in API response", metadata: [
"path": .string(context.codingPath.description),
"rawJSON": .string(String(data: data, encoding: .utf8) ?? "")
])
throw APIError.invalidResponse(reason: "Missing key: \(key)")
} catch {
logger.error("Failed to decode User", error: error)
throw APIError.decodingFailed(error)
}
结果:你发现 API 有时会为已删除的用户省略 email 字段。修复:仅针对该情况使 email 为可选,而不是所有用户。
上下文:日期解析在你的时区有效,但对欧洲 QA 团队失败。
压力:"对我来说有效,QA 肯定做错了什么。"
你会这样合理化的原因:
实际会发生的情况:
"2024-12-14T10:00:00"纪律性回应:
"间歇性日期失败几乎总是时区问题。让我检查我们是否使用带时区偏移的 ISO8601。"
检查:
// ❌ 当前(跨时区失败)
decoder.dateDecodingStrategy = .iso8601
// 服务器发送: "2024-12-14T10:00:00" (无时区)
// PST 设备: Dec 14, 10:00 PST
// CET 设备: Dec 14, 10:00 CET
// 错误: 不同的时间!
// ✅ 修复: 要求服务器发送时区
// "2024-12-14T10:00:00+00:00"
// 或: 显式解析为 UTC
decoder.dateDecodingStrategy = .custom { decoder in
let container = try decoder.singleValueContainer()
let dateString = try container.decode(String.self)
let formatter = ISO8601DateFormatter()
formatter.timeZone = TimeZone(secondsFromGMT: 0) // 强制 UTC
guard let date = formatter.date(from: dateString) else {
throw DecodingError.dataCorruptedError(
in: container,
debugDescription: "Invalid ISO8601 date: \(dateString)"
)
}
return date
}
结果:错误修复,服务器向 API 添加时区(或者你显式解析为 UTC)。不再有间歇性失败。
上下文:新的 API 字段导致解码失败。产品经理希望 1 小时内修复。
压力:"你不能就让那个字段变成可选吗?我们需要发布这个。"
你会这样合理化的原因:
实际会发生的情况:
user.email ?? ""email 是 nil纪律性回应:
"让它变成可选掩盖了真正的问题。让我检查是 API 错了还是我们的模型错了。这需要 10 分钟。"
调查:
// 步骤 1: 打印原始 JSON
do {
let json = try JSONSerialization.jsonObject(with: data)
print(json)
} catch {
print("Invalid JSON: \(error)")
}
// 步骤 2: 检查键是否存在但值为 null
// {"email": null} 与键完全缺失
// 步骤 3: 检查 API 文档 - email 实际上是必需的吗?
常见结果:
结果:你发现 email 在新 API 版本中嵌套在 user.contact.email 中。使用嵌套容器修复,而不是可选性。
// ✅ 正确的修复
struct User: Decodable {
let id: UUID
let email: String // 仍然是必需的
enum CodingKeys: CodingKey {
case id, contact
}
enum ContactKeys: CodingKey {
case email
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(UUID.self, forKey: .id)
let contact = try container.nestedContainer(
keyedBy: ContactKeys.self,
forKey: .contact
)
email = try contact.decode(String.self, forKey: .email)
}
}
Sendable@Model 类型使用 Codable 进行 CloudKit 同步Coder 协议包装 Codable 用于 Network.frameworkAppEnum 参数使用 Codable 序列化: CodableDateFormatter 需要 en_US_POSIX 和显式时区DecodingError 情况核心原则:Codable 是 Swift 的通用序列化协议。掌握一次,随处使用。
每周安装数
91
仓库
GitHub 星标
601
首次出现
2026年1月21日
安全审计
安装于
opencode77
claude-code73
codex71
gemini-cli70
cursor69
github-copilot66
Comprehensive guide to Codable protocol conformance for JSON and PropertyList encoding/decoding in Swift 6.x.
Has your type...
├─ All properties Codable? → Automatic synthesis (just add `: Codable`)
├─ Property names differ from JSON keys? → CodingKeys customization
├─ Needs to exclude properties? → CodingKeys customization
├─ Enum with associated values? → Check enum synthesis patterns
├─ Needs structural transformation? → Manual implementation + bridge types
├─ Needs data not in JSON? → DecodableWithConfiguration (iOS 15+)
└─ Complex nested JSON? → Manual implementation + nested containers
| Error | Solution |
|---|---|
| "Type 'X' does not conform to protocol 'Decodable'" | Ensure all stored properties are Codable |
| "No value associated with key X" | Check CodingKeys match JSON keys |
| "Expected to decode X but found Y instead" | Type mismatch; check JSON structure or use bridge type |
| "keyNotFound" | JSON missing expected key; make property optional or provide default |
| "Date parsing failed" | Configure dateDecodingStrategy on decoder |
Swift automatically synthesizes Codable conformance when all stored properties are Codable.
// ✅ Automatic synthesis
struct User: Codable {
let id: UUID // Codable
var name: String // Codable
var membershipPoints: Int // Codable
}
// JSON: {"id":"...", "name":"Alice", "membershipPoints":100}
Requirements :
enum Direction: String, Codable {
case north, south, east, west
}
// Encodes as: "north"
The raw value itself becomes the JSON representation.
enum Status: Codable {
case success
case failure
case pending
}
// Encodes as: {"success":{}}
Each case becomes an object with the case name as the key and empty dictionary as value.
enum APIResult: Codable {
case success(data: String, count: Int)
case error(code: Int, message: String)
}
// success case encodes as:
// {"success":{"data":"example","count":5}}
Gotcha : Unlabeled associated values generate _0, _1 keys:
enum Command: Codable {
case store(String, Int) // ❌ Unlabeled
}
// Encodes as: {"store":{"_0":"value","_1":42}}
Fix : Always label associated values for predictable JSON:
enum Command: Codable {
case store(key: String, value: Int) // ✅ Labeled
}
// Encodes as: {"store":{"key":"value","value":42}}
Automatic synthesis fails when:
@Published, @State (except @AppStorage with Codable types)init(from:) manuallyUse CodingKeys enum to customize encoding/decoding without full manual implementation.
struct Article: Codable {
let url: URL
let title: String
let body: String
enum CodingKeys: String, CodingKey {
case url = "source_link" // JSON uses "source_link"
case title = "content_name" // JSON uses "content_name"
case body // Matches JSON key
}
}
// JSON: {"source_link":"...", "content_name":"...", "body":"..."}
Omit properties from CodingKeys to exclude them from encoding/decoding:
struct NoteCollection: Codable {
let name: String
let notes: [Note]
var localDrafts: [Note] = [] // ✅ Must have default value
enum CodingKeys: CodingKey {
case name
case notes
// localDrafts omitted - not encoded/decoded
}
}
Rule : Excluded properties require default values or you must implement init(from:) manually.
For consistent snake_case → camelCase conversion:
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
// JSON: {"first_name":"Alice", "last_name":"Smith"}
// Decodes to: User(firstName: "Alice", lastName: "Smith")
Customize keys for enum associated values using {CaseName}CodingKeys:
enum Command: Codable {
case store(key: String, value: Int)
case delete(key: String)
enum StoreCodingKeys: String, CodingKey {
case key = "identifier" // Renames "key" to "identifier"
case value = "data" // Renames "value" to "data"
}
enum DeleteCodingKeys: String, CodingKey {
case key = "identifier"
}
}
// store case encodes as: {"store":{"identifier":"x","data":42}}
Pattern : {CaseName}CodingKeys with capitalized case name.
For structural differences between JSON and Swift models, implement init(from:) and encode(to:).
| Container | When to Use |
|---|---|
| Keyed | Dictionary-like data with string keys |
| Unkeyed | Array-like sequential data |
| Single-value | Wrapper types that encode as a single value |
| Nested | Hierarchical JSON structures |
Flatten hierarchical JSON:
// JSON:
// {
// "latitude": 37.7749,
// "longitude": -122.4194,
// "additionalInfo": {
// "elevation": 52
// }
// }
struct Coordinate {
var latitude: Double
var longitude: Double
var elevation: Double // Nested in JSON, flat in Swift
enum CodingKeys: String, CodingKey {
case latitude, longitude, additionalInfo
}
enum AdditionalInfoKeys: String, CodingKey {
case elevation
}
}
extension Coordinate: Decodable {
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
latitude = try values.decode(Double.self, forKey: .latitude)
longitude = try values.decode(Double.self, forKey: .longitude)
let additionalInfo = try values.nestedContainer(
keyedBy: AdditionalInfoKeys.self,
forKey: .additionalInfo
)
elevation = try additionalInfo.decode(Double.self, forKey: .elevation)
}
}
extension Coordinate: Encodable {
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(latitude, forKey: .latitude)
try container.encode(longitude, forKey: .longitude)
var additionalInfo = container.nestedContainer(
keyedBy: AdditionalInfoKeys.self,
forKey: .additionalInfo
)
try additionalInfo.encode(elevation, forKey: .elevation)
}
}
When JSON structure fundamentally differs from Swift model:
// JSON: {"USD": 1.0, "EUR": 0.85, "GBP": 0.73}
// Want: [ExchangeRate]
struct ExchangeRate {
let currency: String
let rate: Double
}
// Bridge type for decoding
private extension ExchangeRate {
struct List: Decodable {
let values: [ExchangeRate]
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let dictionary = try container.decode([String: Double].self)
values = dictionary.map { ExchangeRate(currency: $0, rate: $1) }
}
}
}
// Public interface
extension ExchangeRate {
static func decode(from data: Data) throws -> [ExchangeRate] {
let list = try JSONDecoder().decode(List.self, from: data)
return list.values
}
}
let decoder = JSONDecoder()
// 1. ISO 8601 (recommended)
decoder.dateDecodingStrategy = .iso8601
// Expects: "2024-02-15T17:00:00+01:00"
// 2. Unix timestamp (seconds)
decoder.dateDecodingStrategy = .secondsSince1970
// Expects: 1708012800
// 3. Unix timestamp (milliseconds)
decoder.dateDecodingStrategy = .millisecondsSince1970
// Expects: 1708012800000
// 4. Custom formatter
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
formatter.locale = Locale(identifier: "en_US_POSIX") // ✅ Always set
formatter.timeZone = TimeZone(secondsFromGMT: 0) // ✅ Always set
decoder.dateDecodingStrategy = .formatted(formatter)
// 5. Custom closure
decoder.dateDecodingStrategy = .custom { decoder in
let container = try decoder.singleValueContainer()
let dateString = try container.decode(String.self)
if let date = ISO8601DateFormatter().date(from: dateString) {
return date
}
throw DecodingError.dataCorruptedError(
in: container,
debugDescription: "Cannot decode date string \(dateString)"
)
}
Default : 2024-02-15T17:00:00+01:00 Timezone required : Without timezone offset, decoding may fail across regions
// ❌ No timezone - parsing depends on device locale
"2024-02-15T17:00:00"
// ✅ With timezone - unambiguous
"2024-02-15T17:00:00+01:00"
Custom closures run for every date - optimize expensive operations:
// ❌ Creates new formatter for every date
decoder.dateDecodingStrategy = .custom { decoder in
let formatter = DateFormatter() // Expensive!
// ...
}
// ✅ Reuse formatter
let sharedFormatter = DateFormatter()
sharedFormatter.dateFormat = "yyyy-MM-dd"
decoder.dateDecodingStrategy = .custom { decoder in
// Use sharedFormatter
}
Handle APIs that encode numbers as strings:
protocol StringRepresentable: CustomStringConvertible {
init?(_ string: String)
}
extension Int: StringRepresentable {}
extension Double: StringRepresentable {}
struct StringBacked<Value: StringRepresentable>: Codable {
var value: Value
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let string = try container.decode(String.self)
guard let value = Value(string) else {
throw DecodingError.dataCorruptedError(
in: container,
debugDescription: "Cannot convert '\(string)' to \(Value.self)"
)
}
self.value = value
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(value.description)
}
}
// Usage
struct Product: Codable {
let name: String
private let _price: StringBacked<Double>
var price: Double {
get { _price.value }
set { _price = StringBacked(value: newValue) }
}
enum CodingKeys: String, CodingKey {
case name
case _price = "price"
}
}
// JSON: {"name":"Widget","price":"19.99"}
// Decodes to: Product(name: "Widget", price: 19.99)
For loosely typed APIs that may return different types:
struct FlexibleValue: Codable {
let stringValue: String
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let string = try? container.decode(String.self) {
stringValue = string
} else if let int = try? container.decode(Int.self) {
stringValue = String(int)
} else if let double = try? container.decode(Double.self) {
stringValue = String(double)
} else {
throw DecodingError.dataCorruptedError(
in: container,
debugDescription: "Cannot decode value to String, Int, or Double"
)
}
}
}
Warning : Avoid this pattern unless the API is truly unpredictable. Prefer strict types.
For types that need data unavailable in JSON:
struct User: Encodable, DecodableWithConfiguration {
let id: UUID
var name: String
var favorites: Favorites // Not in JSON, injected via configuration
enum CodingKeys: CodingKey {
case id, name
}
init(from decoder: Decoder, configuration: Favorites) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(UUID.self, forKey: .id)
name = try container.decode(String.self, forKey: .name)
favorites = configuration // Injected
}
}
// Usage (iOS 17+)
let favorites = try await fetchFavorites()
let user = try JSONDecoder().decode(
User.self,
from: data,
configuration: favorites
)
extension JSONDecoder {
private struct ConfigurationDecodingWrapper<T: DecodableWithConfiguration>: Decodable {
var wrapped: T
init(from decoder: Decoder) throws {
let config = decoder.userInfo[configurationUserInfoKey] as! T.DecodingConfiguration
wrapped = try T(from: decoder, configuration: config)
}
}
func decode<T: DecodableWithConfiguration>(
_ type: T.Type,
from data: Data,
configuration: T.DecodingConfiguration
) throws -> T {
let decoder = JSONDecoder()
decoder.userInfo[Self.configurationUserInfoKey] = configuration
let wrapper = try decoder.decode(ConfigurationDecodingWrapper<T>.self, from: data)
return wrapper.wrapped
}
}
private let configurationUserInfoKey = CodingUserInfoKey(rawValue: "configuration")!
Decode only the fields you need:
struct ArticlePreview: Decodable {
let id: UUID
let title: String
// Omit body, comments, etc.
}
// JSON has many more fields, but we only decode id and title
do {
let user = try decoder.decode(User.self, from: data)
} catch DecodingError.keyNotFound(let key, let context) {
print("Missing key '\(key)' at path: \(context.codingPath)")
} catch DecodingError.typeMismatch(let type, let context) {
print("Type mismatch for \(type) at path: \(context.codingPath)")
} catch DecodingError.valueNotFound(let type, let context) {
print("Value not found for \(type) at path: \(context.codingPath)")
} catch DecodingError.dataCorrupted(let context) {
print("Data corrupted at path: \(context.codingPath)")
} catch {
print("Other error: \(error)")
}
1. Pretty-print JSON
let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
let jsonData = try encoder.encode(user)
print(String(data: jsonData, encoding: .utf8)!)
2. Inspect coding path
// In custom init(from:)
print("Decoding at path: \(decoder.codingPath)")
3. Validate JSON structure
// Quick check: Can it decode as Any?
let json = try JSONSerialization.jsonObject(with: data)
print(json) // See actual structure
| Anti-Pattern | Cost | Better Approach |
|---|---|---|
| Manual JSON string building | Injection vulnerabilities, escaping bugs, no type safety | Use JSONEncoder |
try? swallowing DecodingError | Silent failures, debugging nightmares, data loss | Handle specific error cases |
| Optional properties to avoid decode errors | Runtime crashes, nil checks everywhere, masks structural issues | Fix JSON/model mismatch or use DecodableWithConfiguration |
| Duplicating partial models | 2-5 hours maintenance per change, sync issues, fragile | Use bridge types or configuration |
| Ignoring date timezone |
// ❌ Silent failure - production bug waiting to happen
let user = try? JSONDecoder().decode(User.self, from: data)
// If this fails, user is nil - why? No idea.
// ✅ Explicit error handling
do {
let user = try JSONDecoder().decode(User.self, from: data)
} catch {
logger.error("Failed to decode user: \(error)")
// Now you know WHY it failed
}
Context : API integration deadline tomorrow, decoder failing on some edge case.
Pressure : "We can debug it later, just make it work now."
Why You'll Rationalize :
What Actually Happens :
Discipline Response :
"Using
try?here means we'll lose data silently. Let me spend 5 minutes handling the specific error case. If it's truly rare, I'll log it so we can fix the root cause."
5-Minute Fix :
do {
return try decoder.decode(User.self, from: data)
} catch DecodingError.keyNotFound(let key, let context) {
logger.error("Missing key '\(key)' in API response", metadata: [
"path": .string(context.codingPath.description),
"rawJSON": .string(String(data: data, encoding: .utf8) ?? "")
])
throw APIError.invalidResponse(reason: "Missing key: \(key)")
} catch {
logger.error("Failed to decode User", error: error)
throw APIError.decodingFailed(error)
}
Result : You discover the API sometimes omits the email field for deleted users. Fix: make email optional only for that case, not all users.
Context : Date parsing works in your timezone but fails for European QA team.
Pressure : "It works for me, QA must be doing something wrong."
Why You'll Rationalize :
What Actually Happens :
"2024-12-14T10:00:00"Discipline Response :
"Intermittent date failures are almost always timezone issues. Let me check if we're using ISO8601 with timezone offsets."
Check :
// ❌ Current (fails across timezones)
decoder.dateDecodingStrategy = .iso8601
// Server sends: "2024-12-14T10:00:00" (no timezone)
// PST device: Dec 14, 10:00 PST
// CET device: Dec 14, 10:00 CET
// Bug: Different times!
// ✅ Fix: Require server to send timezone
// "2024-12-14T10:00:00+00:00"
// OR: Explicitly parse as UTC
decoder.dateDecodingStrategy = .custom { decoder in
let container = try decoder.singleValueContainer()
let dateString = try container.decode(String.self)
let formatter = ISO8601DateFormatter()
formatter.timeZone = TimeZone(secondsFromGMT: 0) // Force UTC
guard let date = formatter.date(from: dateString) else {
throw DecodingError.dataCorruptedError(
in: container,
debugDescription: "Invalid ISO8601 date: \(dateString)"
)
}
return date
}
Result : Bug fixed, server adds timezone to API (or you parse explicitly as UTC). No more intermittent failures.
Context : New API field causes decoding to fail. Product manager wants a fix in 1 hour.
Pressure : "Can't you just make that field optional? We need this shipped."
Why You'll Rationalize :
What Actually Happens :
user.email ?? "" everywhereemail was nilDiscipline Response :
"Making it optional masks the real problem. Let me check if the API is wrong or our model is wrong. This will take 10 minutes."
Investigation :
// Step 1: Print raw JSON
do {
let json = try JSONSerialization.jsonObject(with: data)
print(json)
} catch {
print("Invalid JSON: \(error)")
}
// Step 2: Check if key exists but value is null
// {"email": null} vs key missing entirely
// Step 3: Check API docs - is email actually required?
Common Outcomes :
Result : You discover email is nested in user.contact.email in the new API version. Fix with nested container, not optionality.
// ✅ Correct fix
struct User: Decodable {
let id: UUID
let email: String // Still required
enum CodingKeys: CodingKey {
case id, contact
}
enum ContactKeys: CodingKey {
case email
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(UUID.self, forKey: .id)
let contact = try container.nestedContainer(
keyedBy: ContactKeys.self,
forKey: .contact
)
email = try contact.decode(String.self, forKey: .email)
}
}
Sendable@Model types use Codable for CloudKit syncCoder protocol wraps Codable for Network.frameworkAppEnum parameters use Codable serialization: Codable when structure matches JSONDateFormatter requires en_US_POSIX and explicit timezoneDecodingError cases explicitlyCore Principle : Codable is Swift's universal serialization protocol. Master it once, use it everywhere.
Weekly Installs
91
Repository
GitHub Stars
601
First Seen
Jan 21, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
opencode77
claude-code73
codex71
gemini-cli70
cursor69
github-copilot66
React 组合模式指南:Vercel 组件架构最佳实践,提升代码可维护性
116,600 周安装
| Intermittent bugs across regions, data corruption |
| Always use ISO8601 with timezone or explicit UTC |
JSONSerialization for Codable types | 3x more boilerplate, manual type casting, error-prone | Use JSONDecoder/JSONEncoder |
| No locale on DateFormatter | Parsing fails in non-US locales | Set locale = Locale(identifier: "en_US_POSIX") |