swift-codable

swift-codable

热门

实现 Swift Codable 模型,用于 JSON 和属性列表的编码和解码,使用 JSONDecoder、JSONEncoder、CodingKeys 以及自定义的 init(from:) 或 encode(to:)。在解析 API 响应、重新映射键、展平嵌套 JSON、处理日期或数据解码策略、解码异构数组,或将 Codable 与 URLSession、SwiftData 或 UserDefaults 集成时使用。

944Star
47Fork
更新于 2026/7/15
SKILL.md
只读
名称
swift-codable
描述

实现 Swift Codable 模型,用于 JSON 和属性列表的编码和解码,使用 JSONDecoder、JSONEncoder、CodingKeys 以及自定义的 init(from:) 或 encode(to:)。在解析 API 响应、重新映射键、展平嵌套 JSON、处理日期或数据解码策略、解码异构数组,或将 Codable 与 URLSession、SwiftData 或 UserDefaults 集成时使用。

Swift Codable

使用 CodableEncodable & Decodable)以及 JSONEncoderJSONDecoder 和相关 API 对 Swift 类型进行编码和解码。目标平台为 Swift 6.3 / iOS 26+。

目录

解码与验证工作流

  1. 解码代表性的成功、缺失、null、格式错误、缩写键和日期测试数据。
  2. 失败时,检查 DecodingError、其 codingPath 和原始负载。
  3. 仅修正不匹配的模型、键、容器或策略;不要用有损解码隐藏契约失败。
  4. 重新运行测试数据,并在契约要求双向时进行编码/解码往返。

基本遵循

当所有存储属性本身都是 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_urlbase_uriuser_id 仅匹配 imageUrlbaseUriuserId。如果 Swift 模型使用 imageURLbaseURIuserID,请声明显式的 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 偏好设置

参考资料