实现 Swift Codable 模型,用于 JSON 和属性列表的编码和解码,使用 JSONDecoder、JSONEncoder、CodingKeys 以及自定义的 init(from:) 或 encode(to:)。在解析 API 响应、重新映射键、展平嵌套 JSON、处理日期或数据解码策略、解码异构数组,或将 Codable 与 URLSession、SwiftData 或 UserDefaults 集成时使用。
Swift Codable
使用 Codable(Encodable & Decodable)以及 JSONEncoder、JSONDecoder 和相关 API 对 Swift 类型进行编码和解码。目标平台为 Swift 6.3 / iOS 26+。
目录
- 解码与验证工作流
- 基本遵循
- 自定义 CodingKeys
- 自定义解码和编码
- 嵌套和展平容器
- 异构数组
- 日期解码策略
- 数据和键策略
- 有损数组解码
- 单值容器
- 缺失键的默认值
- 编码器和解码器配置
- Codable 与 URLSession
- Codable 与 SwiftData
- Codable 与 UserDefaults
- 常见错误
- 审查清单
- 参考资料
解码与验证工作流
- 解码代表性的成功、缺失、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)
// 将 Unix 时间戳解码为 Double,转换为 Date
let epoch = try container.decode(Double.self, forKey: .timestamp)
timestamp = Date(timeIntervalSince1970: epoch)
// 当键缺失时默认为空数组
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(例如 "2024-03-15T10:30:00Z")
decoder.dateDecodingStrategy = .iso8601
// Unix 时间戳(秒)(例如 1710499800)
decoder.dateDecodingStrategy = .secondsSince1970
// 自定义 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)
// 自定义闭包以支持多种格式
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: "无法解码日期:\(string)")
}
在 JSONEncoder 上设置匹配的策略:encoder.dateEncodingStrategy = .iso8601
数据和键策略
let decoder = JSONDecoder()
decoder.dataDecodingStrategy = .base64 // Base64 编码的 Data 字段
decoder.keyDecodingStrategy = .convertFromSnakeCase // 仅适用于简单键;不适用于 URL/ID 拼写
// {"user_name": "Alice"} 映射到 `var userName: String` -- 无需 CodingKeys
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 值分配显式回退行为时,加载 缺失键默认值。
编码器和解码器配置
在传输/文件格式边界保持匹配的策略。加载 编码器配置 以了解非标准浮点数和属性列表指南。
Codable 与 URLSession
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)
}
// 通用 API 信封。在此辅助函数内部配置解码器,因为
// fetchUser 的解码器超出作用域。
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
}
Codable 与 SwiftData
保持模式值类型化,并将持久化设计路由到 swiftdata;参见 持久化边界。
Codable 与 UserDefaults
对于小型偏好设置使用原始类型。加载 持久化边界 以了解小型 Codable RawRepresentable/@AppStorage 交接;对于较大或持久数据,使用真正的持久化层。
常见错误
1. 未处理缺失的默认字段:
// 不要 -- 如果键不存在会崩溃
let value = try container.decode(String.self, forKey: .bio)
// 要 -- 当键缺失或为 null 时回退
let value = try container.decodeIfPresent(String.self, forKey: .bio) ?? ""
2. 一个元素无效导致整个数组失败:
// 不要 -- 一个坏元素导致整个解码失败
let items = try container.decode([Item].self, forKey: .items)
// 要 -- 仅当允许部分成功时单独解码元素
3. 日期策略不匹配:
// 不要 -- 默认策略期望 Double,但 API 发送 ISO 字符串
let decoder = JSONDecoder() // dateDecodingStrategy 默认为 .deferredToDate
// 要 -- 设置策略以匹配您的 API 格式
decoder.dateDecodingStrategy = .iso8601
4. 强制解包解码的可选值:
// 不要
let user = try? decoder.decode(User.self, from: data)
print(user!.name)
// 要
guard let user = try? decoder.decode(User.self, from: data) else { return }
5. 仅需要 Decodable 时使用 Codable:
// 不要 -- 不必要地约束类型也必须可编码
struct APIResponse: Codable { let id: Int; let message: String }
// 要 -- 对于只读 API 响应使用 Decodable
struct APIResponse: Decodable { let id: Int; let message: String }
6. 为简单的 snake_case API 手动编写 CodingKeys:
// 不要 -- 每个模型都有冗长的样板代码
enum CodingKeys: String, CodingKey {
case userName = "user_name"
case avatarUrl = "avatar_url"
}
// 要 -- 对于简单情况,在解码器上配置一次
decoder.keyDecodingStrategy = .convertFromSnakeCase
// 对于 `imageURL`、`baseURI`、`userID` 等名称保留 CodingKeys。
审查清单
- [ ] 仅当不需要编码时,类型遵循
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 模型属性兼容性






