关于 axiom-codable
面向 Swift 6.x 开发者的 Codable 参考技能,介绍 JSON 与 PropertyList 的编码和解码。内容涵盖自动合成、键定制、手动转换、日期处理及错误诊断。
主要功能
- 01Codable 自动合成与枚举
- 02CodingKeys 键定制
- 03容器与桥接类型转换
- 04日期解码策略
- 05解码错误诊断
适用场景
- 01映射不同命名的 JSON 键
- 02解码嵌套或结构不匹配的数据
- 03处理 API 日期与类型变化
- 04排查 Codable 解码失败
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ charleswiltgen/ axiom --skill axiom-codable安装后在 Claude Code 中输入/axiom-codable确认已加载。
详细介绍
Swift Codable 模式
Swift 6.x 中 JSON 和 PropertyList 编码/解码的 Codable 协议符合性完整指南。
快速参考
决策树:何时使用各方法
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
常见触发条件
| 错误 | 解决方案 |
|---|---|
| "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 |
第 1 部分:自动合成
当所有存储属性都符合 Codable 时,Swift 会自动合成 Codable 符合性。
结构体合成
// ✅ Automatic synthesis
struct User: Codable {
let id: UUID // Codable
var name: String // Codable
var membershipPoints: Int // Codable
}
// JSON: {"id":"...", "name":"Alice", "membershipPoints":100}
要求:
- 所有存储属性都必须符合 Codable
- 属性使用标准 Swift 类型或其他 Codable 类型
- 无需自定义初始化逻辑
枚举合成模式
模式 1:原始值枚举
enum Direction: String, Codable {
case north, south, east, west
}
// Encodes as: "north"
原始值本身会成为 JSON 表示形式。
模式 2:不带关联值的枚举
enum Status: Codable {
case success
case failure
case pending
}
// Encodes as: {"success":{}}
每个枚举项都会成为一个对象,其键为枚举项名称,值为空字典。
模式 3:带关联值的枚举
enum APIResult: Codable {
case success(data: String, count: Int)
case error(code: Int, message: String)
}
// success case encodes as:
// {"success":{"data":"example","count":5}}
注意事项:未标记的关联值会生成 _0、_1 键:
enum Command: Codable {
case store(String, Int) // ❌ Unlabeled
}
// Encodes as: {"store":{"_0":"value","_1":42}}
修复方法:始终为关联值添加标签,以确保 JSON 结构可预测:
enum Command: Codable {
case store(key: String, value: Int) // ✅ Labeled
}
// Encodes as: {"store":{"key":"value","value":42}}
合成失效的情况
以下情况会导致自动合成失败:
- 计算属性 - 只会编码存储属性
- 不符合 Codable 的属性 - 未符合 Codable 的自定义类型
- 属性包装器 -
@Published、@State(符合 Codable 的类型搭配@AppStorage时除外) - 类继承 - 子类必须手动实现
init(from:)
第 2 部分:自定义 CodingKeys
使用 CodingKeys 枚举自定义编码/解码,无需完整的手动实现。
重命名键
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":"..."}
排除属性
从 CodingKeys 中省略属性,即可将其排除在编码/解码之外:
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
}
}
规则:被排除的属性必须有默认值,否则必须手动实现 init(from:)。
蛇形命名转换
如需一致地将 snake_case 转换为 camelCase:
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
// JSON: {"first_name":"Alice", "last_name":"Smith"}
// Decodes to: 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" // 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}}
模式:{CaseName}CodingKeys 中的枚举项名称首字母大写。
第 3 部分:手动实现
当 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 // 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)
}
}
用于结构不匹配的桥接类型
当 JSON 结构与 Swift 模型存在根本差异时:
// 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
}
}
第 4 部分:日期处理
内置策略
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)"
)
}
ISO 8601 的细节
默认格式:2024-02-15T17:00:00+01:00
必须包含时区:如果没有时区偏移,跨区域解码可能会失败
// ❌ No timezone - parsing depends on device locale
"2024-02-15T17:00:00"
// ✅ With timezone - unambiguous
"2024-02-15T17:00:00+01:00"
性能注意事项
自定义闭包会为每个日期运行 - 优化开销较大的操作:
// ❌ 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
}
第 5 部分:类型转换
StringBacked 包装器
处理将数字编码为字符串的 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)
}
}
// 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)
类型强制转换
适用于可能返回不同类型的弱类型 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 的返回类型确实无法预测,否则请避免使用此模式。优先使用严格类型。
第 6 部分:高级模式
DecodableWithConfiguration(iOS 15+)
适用于需要使用 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
)
userInfo 变通方案(iOS 15-16)
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
// Omit body, comments, etc.
}
// JSON has many more fields, but we only decode id and title
第 7 部分:调试
DecodingError 情况
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. 检查编码路径
// In custom init(from:)
print("Decoding at path: \(decoder.codingPath)")
3. 验证 JSON 结构
// Quick check: Can it decode as Any?
let json = try JSONSerialization.jsonObject(with: data)
print(json) // See actual structure
反模式
| 反模式 | 代价 | 更好的做法 |
|---|---|---|
| 手动构建 JSON 字符串 | 注入漏洞、转义错误、没有类型安全 | 使用 JSONEncoder |
用 try? 吞掉 DecodingError | 静默失败、调试噩梦、数据丢失 | 处理特定错误情况 |
| 为了避免解码错误而使用可选属性 | 运行时崩溃、到处都要检查 nil、掩盖结构问题 | 修复 JSON/模型不匹配问题,或使用 DecodableWithConfiguration |
| 重复定义部分模型 | 每次变更需花费 2-5 小时维护、同步问题、脆弱 | 使用桥接类型或配置 |
| 忽略日期时区 | 不同地区出现间歇性错误、数据损坏 | 始终使用带时区的 ISO8601,或显式指定 UTC |
对 Codable 类型使用 JSONSerialization | 多出 3 倍样板代码、手动类型转换、容易出错 | 使用 JSONDecoder/JSONEncoder |
| DateFormatter 未设置区域设置 | 在非美国区域设置下解析失败 | 设置 locale = Locale(identifier: "en_US_POSIX") |
为什么 try? 很危险
// ❌ 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
}
压力场景
场景 1:“直接用 try? 让它编译通过”
背景:API 集成截止日期是明天,解码器在某个边缘情况下失败。
压力:“我们之后再调试,现在先让它能用。”
你可能会找的借口:
- “只有 1% 的请求会失败”
- “之后可以再加日志”
- “客户不会注意到”
实际会发生的情况:
- 那 1% 的数据会悄无声息地丢失
- 没有日志,所以无法在生产环境中调试
- 3 个月后才收到客户投诉
- 到那时你已经忘了当时的背景
应对之道:
“在这里使用
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 设为可选,而不是对所有用户都如此。
场景 2:“日期解析偶尔失败,肯定是服务器的问题”
背景:在你的时区日期解析正常,但欧洲 QA 团队解析失败。
压力:“我这里运行正常,肯定是 QA 做错了什么。”
你可能会找的借口:
- “我的测试在本地都能通过”
- “服务器可能发送了错误数据”
- “是他们的设备设置有问题”
实际会发生的情况:
- 服务器发送的日期没有时区:
"2024-12-14T10:00:00" - 你的设备(PST)将其解析为 10:00 PST
- QA 设备(CET)将其解析为 10:00 CET
- 绝对时间不同,导致间歇性错误
应对之道:
“间歇性的日期解析失败几乎总是时区问题。让我检查一下我们是否在使用带时区偏移的 ISO8601。”
检查:
// ❌ 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
}
结果:问题修复,服务器为 API 添加时区(或者你显式地按 UTC 解析)。不再出现间歇性失败。
场景 3:“让它变成可选项就行”
背景:新增的 API 字段导致解码失败。产品经理要求 1 小时内修复。
压力:“你不能直接把这个字段设为可选吗?我们得赶紧发布。”
你会为自己找借口:
- “这比修复 API 快”
- “之后可以再把它改回必选”
- “用户不会注意到”
实际会发生的事:
- 这个字段实际上是该功能必需的
- 你到处添加
user.email ?? "" - 3 个月后:由于
email为 nil,生产环境发生崩溃 - 现在你已经想不起来当初为什么把它设为可选了
严谨应对:
“把它设为可选会掩盖真正的问题。我来确认是 API 有误,还是我们的模型有误。这只需要 10 分钟。”
调查:
// 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?
常见结果:
- API 有误:字段本应存在 → 提交缺陷报告,获取热修复
- 模型有误:某些流程中该字段是可选的 → 按正确的可选性处理,并编写清晰的文档
- 结构不匹配:字段嵌套在其他字段中 → 使用嵌套容器
结果:你发现,在新版 API 中,email 嵌套在 user.contact.email 中。使用嵌套容器修复,而不是将其设为可选。
// ✅ 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)
}
}
相关技能
- swift-concurrency — 跨越 Actor 边界的 Codable 类型必须是
Sendable - swiftdata —
@Model类型使用 Codable 进行 CloudKit 同步 - networking —
Coder协议为 Network.framework 封装 Codable - app-intents-ref —
AppEnum参数使用 Codable 序列化
关键要点
- 优先使用自动合成 — 结构与 JSON 匹配时添加
: Codable - 简单的不匹配使用 CodingKeys — 无需手动编写代码即可重命名或排除字段
- 结构存在差异时手动实现 — 使用嵌套容器、桥接类型
- 始终设置区域设置和时区 —
DateFormatter需要en_US_POSIX和显式时区 - 绝不要用 try? 吞掉错误 — 明确处理
DecodingError情况 - Codable + Sendable — 值类型(struct/enum)非常适合异步网络通信
核心原则:Codable 是 Swift 通用的序列化协议。掌握一次,处处可用。