关于 swift-codable
swift-codable 是面向 Swift 开发者的 Codable 编码与解码技能,涵盖 JSONEncoder 和 JSONDecoder 的常见用法。它侧重处理自定义映射、格式策略及解码错误,并提供 API 与持久化场景的参考。
主要功能
- 01Codable 类型的编码与解码
- 02自定义键及编码逻辑
- 03嵌套和扁平容器处理
- 04日期、数据与键策略
- 05缺失键和数组解码处理
适用场景
- 01解析 API 的 JSON 响应
- 02将 Swift 类型编码为 JSON
- 03排查字段、日期或解码错误
- 04为持久化边界选择 Codable 用法
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ dpearson2699/ swift-ios-skills --skill swift-codable安装后在 Claude Code 中输入/swift-codable确认已加载。
附带文件evals/evals.jsonreferences/codable-advanced-patterns.md
详细介绍
Swift Codable
使用 Codable(Encodable & Decodable)、JSONEncoder、JSONDecoder 及相关 API 对 Swift 类型进行编码和解码。面向 Swift 6.3 / iOS 26+。
内容
- 解码与验证工作流
- 基本遵循
- 自定义 CodingKeys
- 自定义解码与编码
- 嵌套与扁平化容器
- 异构数组
- 日期解码策略
- Data 与键策略
- 有损数组解码
- 单值容器
- 缺失键的默认值
- 编码器与解码器配置
- 使用 URLSession 的 Codable
- 使用 SwiftData 的 Codable
- 使用 UserDefaults 的 Codable
- 常见错误
- 检查清单
- 参考资料
解码与验证工作流
- 解码具有代表性的成功、缺失、null、格式错误、缩写键和日期样例。
- 失败时,检查
DecodingError、其codingPath和原始负载。 - 仅修正不匹配的模型、键、容器或策略;不要用有损解码掩盖契约失败。
- 重新运行样例,并在契约涵盖双向操作时进行编码/解码往返测试。
基本遵循
当所有存储属性本身都遵循 Codable 时,编译器会自动合成遵循实现:
struct User: Codable {
let id: Int
let name: String
let email: String
let isVerified: Bool
}
let user = try JSONDecoder().decode(User.self, from: jsonData)
let encoded = try JSONEncoder().encode(user)
只读 API 响应优先使用 Decodable,只写场景使用 Encodable。只有在需要双向操作时才使用 Codable。
自定义 CodingKeys
声明 CodingKeys 枚举,无需编写自定义解码器即可重命名 JSON 键:
struct Product: Codable {
let id: Int
let displayName: String
let imageURL: URL
let priceInCents: Int
enum CodingKeys: String, CodingKey {
case id
case displayName = "display_name"
case imageURL = "image_url"
case priceInCents = "price_in_cents"
}
}
每个存储属性都必须出现在枚举中。从 CodingKeys 中省略某个属性会将其排除在编码/解码之外——请为其提供默认值或单独计算。
自定义解码与编码
对于合成遵循无法处理的转换,重写 init(from:) 和 encode(to:):
struct Event: Codable {
let name: String
let timestamp: Date
let tags: [String]
enum CodingKeys: String, CodingKey {
case name, timestamp, tags
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name)
// Decode Unix timestamp as Double, convert to Date
let epoch = try container.decode(Double.self, forKey: .timestamp)
timestamp = Date(timeIntervalSince1970: epoch)
// Default to empty array when key is missing
tags = try container.decodeIfPresent([String].self, forKey: .tags) ?? []
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .name)
try container.encode(timestamp.timeIntervalSince1970, forKey: .timestamp)
try container.encode(tags, forKey: .tags)
}
}
嵌套与扁平化容器
使用 nestedContainer(keyedBy:forKey:) 遍历并扁平化嵌套 JSON:
// JSON: { "id": 1, "location": { "lat": 37.7749, "lng": -122.4194 } }
struct Place: Decodable {
let id: Int
let latitude: Double
let longitude: Double
enum CodingKeys: String, CodingKey { case id, location }
enum LocationKeys: String, CodingKey { case lat, lng }
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(Int.self, forKey: .id)
let location = try container.nestedContainer(
keyedBy: LocationKeys.self, forKey: .location)
latitude = try location.decode(Double.self, forKey: .lat)
longitude = try location.decode(Double.self, forKey: .lng)
}
}
串联多次 nestedContainer 调用以扁平化深层嵌套结构。嵌套数组也可使用 nestedUnkeyedContainer(forKey:)。
异构数组
加载高级 Codable 模式,了解基于判别字段的混合数组。
日期解码策略
配置 JSONDecoder.dateDecodingStrategy 以匹配 API:
let decoder = JSONDecoder()
// ISO 8601 (e.g., "2024-03-15T10:30:00Z")
decoder.dateDecodingStrategy = .iso8601
// Unix timestamp in seconds (e.g., 1710499800)
decoder.dateDecodingStrategy = .secondsSince1970
// Custom DateFormatter
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = TimeZone(secondsFromGMT: 0)
decoder.dateDecodingStrategy = .formatted(formatter)
// Custom closure for multiple formats
decoder.dateDecodingStrategy = .custom { decoder in
let container = try decoder.singleValueContainer()
let string = try container.decode(String.self)
if let date = ISO8601DateFormatter().date(from: string) { return date }
throw DecodingError.dataCorruptedError(
in: container, debugDescription: "Cannot decode date: \(string)")
}
在 JSONEncoder 上设置匹配的策略:
encoder.dateEncodingStrategy = .iso8601
Data 与键策略
let decoder = JSONDecoder()
decoder.dataDecodingStrategy = .base64 // Base64-encoded Data fields
decoder.keyDecodingStrategy = .convertFromSnakeCase // simple keys only; not URL/ID spelling
// {"user_name": "Alice"} maps to `var userName: String` -- no CodingKeys needed
let encoder = JSONEncoder()
encoder.dataEncodingStrategy = .base64
encoder.keyEncodingStrategy = .convertToSnakeCase
仅在机械地将 snake_case 转换为 camelCase 时使用键策略。
convertFromSnakeCase 按拼写映射,而非遵循 Swift 的缩写/首字母缩略词规则:
image_url、base_uri 和 user_id 仅匹配 imageUrl、baseUri 和
userId。如果 Swift 模型使用 imageURL、baseURI 或 userID,
请声明显式的 CodingKeys;该策略不会合成这些名称。
有损数组解码
仅当产品契约允许部分成功时才使用有损数组;请加载 有损数组。
单值容器
使用 singleValueContainer() 创建类型安全的基本类型包装;参见
单值包装。
缺失键的默认值
存储属性的默认值不会让合成解码容忍缺失的非可选键。当契约为缺失值或 null 值指定了明确的回退行为时,请加载缺失键默认值。
编码器和解码器配置
在传输层或文件格式边界处理匹配策略。对于非标准浮点数和属性列表的指导,请参阅 编码器配置。
使用 URLSession 处理 Codable
func fetchUser(id: Int) async throws -> User {
let url = URL(string: "https://api.example.com/users/\(id)")!
let (data, response) = try await URLSession.shared.data(from: url)
guard let http = response as? HTTPURLResponse,
(200...299).contains(http.statusCode) else {
throw APIError.invalidResponse
}
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
decoder.dateDecodingStrategy = .iso8601
return try decoder.decode(User.self, from: data)
}
// Generic API envelope. Configure a decoder inside this helper because
// fetchUser's decoder is out of scope.
struct APIResponse<T: Decodable>: Decodable {
let data: T
let meta: Meta?
struct Meta: Decodable { let page: Int; let totalPages: Int }
}
func decodeUsersEnvelope(from data: Data) throws -> [User] {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
decoder.dateDecodingStrategy = .iso8601
return try decoder.decode(APIResponse<[User]>.self, from: data).data
}
使用 SwiftData 处理 Codable
保持架构值的类型明确,并将持久化设计交由 swiftdata 处理;请参阅
持久化边界。
使用 UserDefaults 处理 Codable
小型偏好设置使用基本类型。对于小型 Codable RawRepresentable/@AppStorage 衔接方案,请参阅
持久化边界;
对于更大的数据或需要长期保存的数据,请使用真正的持久化层。
常见错误
1. 未处理缺失的默认值字段:
// DON'T -- crashes if key is absent
let value = try container.decode(String.self, forKey: .bio)
// DO -- falls back when the key is absent or null
let value = try container.decodeIfPresent(String.self, forKey: .bio) ?? ""
2. 一个元素无效导致整个数组解码失败:
// DON'T -- one bad element kills the whole decode
let items = try container.decode([Item].self, forKey: .items)
// DO -- decode elements individually only when partial success is allowed
3. 日期策略不匹配:
// DON'T -- default strategy expects Double, but API sends ISO string
let decoder = JSONDecoder() // dateDecodingStrategy defaults to .deferredToDate
// DO -- set strategy to match your API format
decoder.dateDecodingStrategy = .iso8601
4. 对解码后的可选值强制解包:
// DON'T
let user = try? decoder.decode(User.self, from: data)
print(user!.name)
// DO
guard let user = try? decoder.decode(User.self, from: data) else { return }
5. 只需要 Decodable 时却使用 Codable:
// DON'T -- unnecessarily constrains the type to also be Encodable
struct APIResponse: Codable { let id: Int; let message: String }
// DO -- use Decodable for read-only API responses
struct APIResponse: Decodable { let id: Int; let message: String }
6. 为简单的 snake_case API 手动编写 CodingKeys:
// DON'T -- verbose boilerplate for every model
enum CodingKeys: String, CodingKey {
case userName = "user_name"
case avatarUrl = "avatar_url"
}
// DO -- configure once on the decoder for simple cases
decoder.keyDecodingStrategy = .convertFromSnakeCase
// Keep CodingKeys for `imageURL`, `baseURI`, `userID`, and similar names.
审核清单
- 不需要编码时,类型仅遵循
Decodable - 对可选键或缺失键使用带默认值的
decodeIfPresent - 对简单的 snake_case API 使用
keyDecodingStrategy = .convertFromSnakeCase,并为缩写形式保留 CodingKeys -
dateDecodingStrategy与 API 日期格式匹配 - 对不可靠数据的数组使用有损解码,以跳过无效元素
- 自定义
init(from:)用于验证和转换数据,而非在解码后修正 -
JSONEncoder.outputFormatting包含.sortedKeys,以确保测试输出具有确定性 - 包装类型(UserID 等)使用
singleValueContainer,以生成简洁的 JSON - 使用通用的
APIResponse<T>包装器,统一处理 API 信封 - 不对解码后的值进行强制解包
- 明确持久化边界:SwiftData 仅用于兼容的非计算模型属性,
@AppStorage/UserDefaults 仅用于小型基本类型或RawRepresentable偏好设置
参考资料
- 高级 Codable 模式 -- 混合数组、有损解码、包装器、默认值、配置和持久化边界
- Codable -- 组合 Encodable 和 Decodable 的协议
- JSONDecoder -- 将 JSON 数据解码为 Codable 类型
- JSONEncoder -- 将 Codable 类型编码为 JSON 数据
- CodingKey -- 用于编码/解码键的协议
- JSONDecoder.KeyDecodingStrategy.convertFromSnakeCase -- snake-case 转换行为和限制
- 编码和解码自定义类型 -- Apple 关于自定义 Codable 遵循的指南
- 在自定义类型中使用 JSON -- Apple 提供的 JSON 模式示例代码
- 跨应用启动保留应用的模型数据 -- SwiftData 模型属性兼容性