axiom-codable

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Swift Codable Patterns

Swift Codable 模式

Comprehensive guide to Codable protocol conformance for JSON and PropertyList encoding/decoding in Swift 6.x.
Swift 6.x中JSON与PropertyList编解码的Codable协议一致性综合指南。

Quick Reference

快速参考

Decision Tree: When to Use Each Approach

决策树:何时使用每种方案

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
你的类型是否...
├─ 所有属性都遵循Codable? → 自动合成(只需添加`: Codable`)
├─ 属性名称与JSON键不同? → 自定义CodingKeys
├─ 需要排除部分属性? → 自定义CodingKeys
├─ 带关联值的枚举? → 查看枚举合成模式
├─ 需要结构转换? → 手动实现 + 桥接类型
├─ 需要JSON中不存在的数据? → 使用DecodableWithConfiguration(iOS 15+)
└─ 复杂嵌套JSON? → 手动实现 + 嵌套容器

Common Triggers

常见问题与解决方案

ErrorSolution
"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

错误解决方案
"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

Part 1: Automatic Synthesis

第一部分:自动合成

Swift automatically synthesizes Codable conformance when all stored properties are Codable.
当所有存储属性都遵循Codable时,Swift会自动合成Codable一致性。

Struct Synthesis

结构体合成

swift
// ✅ Automatic synthesis
struct User: Codable {
    let id: UUID              // Codable
    var name: String          // Codable
    var membershipPoints: Int // Codable
}

// JSON: {"id":"...", "name":"Alice", "membershipPoints":100}
Requirements:
  • All stored properties must conform to Codable
  • Properties use standard Swift types or other Codable types
  • No custom initialization logic needed
swift
// ✅ 自动合成
struct User: Codable {
    let id: UUID              // 遵循Codable
    var name: String          // 遵循Codable
    var membershipPoints: Int // 遵循Codable
}

// JSON: {"id":"...", "name":"Alice", "membershipPoints":100}
要求:
  • 所有存储属性必须遵循Codable
  • 属性使用标准Swift类型或其他遵循Codable的类型
  • 无需自定义初始化逻辑

Enum Synthesis Patterns

枚举合成模式

Pattern 1: Raw Value Enums

模式1:原始值枚举

swift
enum Direction: String, Codable {
    case north, south, east, west
}

// Encodes as: "north"
The raw value itself becomes the JSON representation.
swift
enum Direction: String, Codable {
    case north, south, east, west
}

// 编码结果: "north"
原始值本身会成为JSON表示。

Pattern 2: Enums Without Associated Values

模式2:无关联值的枚举

swift
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.
swift
enum Status: Codable {
    case success
    case failure
    case pending
}

// 编码结果: {"success":{}}
每个枚举case会成为一个对象,case名称作为键,空字典作为值。

Pattern 3: Enums With Associated Values

模式3:带关联值的枚举

swift
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:
swift
enum Command: Codable {
    case store(String, Int)  // ❌ Unlabeled
}

// Encodes as: {"store":{"_0":"value","_1":42}}
Fix: Always label associated values for predictable JSON:
swift
enum Command: Codable {
    case store(key: String, value: Int)  // ✅ Labeled
}

// Encodes as: {"store":{"key":"value","value":42}}
swift
enum APIResult: Codable {
    case success(data: String, count: Int)
    case error(code: Int, message: String)
}

// success case编码结果:
// {"success":{"data":"example","count":5}}
注意事项:未标记的关联值会生成
_0
_1
这样的键:
swift
enum Command: Codable {
    case store(String, Int)  // ❌ 未标记
}

// 编码结果: {"store":{"_0":"value","_1":42}}
修复方案:始终为关联值添加标签,以获得可预测的JSON结构:
swift
enum Command: Codable {
    case store(key: String, value: Int)  // ✅ 已标记
}

// 编码结果: {"store":{"key":"value","value":42}}

When Synthesis Breaks

自动合成失效的场景

Automatic synthesis fails when:
  1. Computed properties - Only stored properties are encoded
  2. Non-Codable properties - Custom types without Codable conformance
  3. Property wrappers -
    @Published
    ,
    @State
    (except
    @AppStorage
    with Codable types)
  4. Class inheritance - Subclasses must implement
    init(from:)
    manually

自动合成会在以下情况失效:
  1. 计算属性 - 只有存储属性会被编码
  2. 非Codable属性 - 未遵循Codable的自定义类型
  3. 属性包装器 -
    @Published
    @State
    @AppStorage
    搭配Codable类型除外)
  4. 类继承 - 子类必须手动实现
    init(from:)

Part 2: CodingKeys Customization

第二部分:自定义CodingKeys

Use
CodingKeys
enum to customize encoding/decoding without full manual implementation.
使用
CodingKeys
枚举在无需完全手动实现的情况下自定义编解码逻辑。

Renaming Keys

重命名键

swift
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":"..."}
swift
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":"..."}

Excluding Properties

排除属性

Omit properties from
CodingKeys
to exclude them from encoding/decoding:
swift
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.
CodingKeys
中省略属性,即可将其排除在编解码流程之外:
swift
struct NoteCollection: Codable {
    let name: String
    let notes: [Note]
    var localDrafts: [Note] = []  // ✅ 必须提供默认值

    enum CodingKeys: CodingKey {
        case name
        case notes
        // 省略localDrafts - 不会被编解码
    }
}
规则:被排除的属性需要默认值,或者你必须手动实现
init(from:)

Snake Case Conversion

蛇形命名转换

For consistent snake_case → camelCase conversion:
swift
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase

// JSON: {"first_name":"Alice", "last_name":"Smith"}
// Decodes to: User(firstName: "Alice", lastName: "Smith")
实现蛇形命名(snake_case)到驼峰命名(camelCase)的一致转换:
swift
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase

// JSON: {"first_name":"Alice", "last_name":"Smith"}
// 解码结果: User(firstName: "Alice", lastName: "Smith")

Enum Associated Value Keys

枚举关联值的键自定义

Customize keys for enum associated values using
{CaseName}CodingKeys
:
swift
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.

使用
{CaseName}CodingKeys
自定义枚举关联值的键:
swift
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}}
模式:使用首字母大写的case名称搭配
CodingKeys
,即
{CaseName}CodingKeys

Part 3: Manual Implementation

第三部分:手动实现

For structural differences between JSON and Swift models, implement
init(from:)
and
encode(to:)
.
当JSON与Swift模型存在结构差异时,实现
init(from:)
encode(to:)
方法。

Container Types

容器类型

ContainerWhen to Use
KeyedDictionary-like data with string keys
UnkeyedArray-like sequential data
Single-valueWrapper types that encode as a single value
NestedHierarchical JSON structures
容器类型使用场景
Keyed带字符串键的字典类数据
Unkeyed数组类的顺序数据
Single-value编码为单一值的包装类型
Nested层级化的JSON结构

Nested Containers Example

嵌套容器示例

Flatten hierarchical JSON:
swift
// 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:
// {
//   "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)
    }
}

Bridge Types for Structural Mismatches

结构不匹配时的桥接类型

When JSON structure fundamentally differs from Swift model:
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
    }
}

当JSON结构与Swift模型存在根本性差异时:
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
    }
}

Part 4: Date Handling

第四部分:日期处理

Built-in Strategies

内置策略

swift
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)"
    )
}
swift
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: "无法解码日期字符串 \(dateString)"
    )
}

ISO 8601 Nuances

ISO 8601的细节

Default:
2024-02-15T17:00:00+01:00
Timezone required: Without timezone offset, decoding may fail across regions
swift
// ❌ No timezone - parsing depends on device locale
"2024-02-15T17:00:00"

// ✅ With timezone - unambiguous
"2024-02-15T17:00:00+01:00"
默认格式:
2024-02-15T17:00:00+01:00
必须包含时区: 如果没有时区偏移,跨地区解码可能失败
swift
// ❌ 无时区 - 解析结果依赖设备区域设置
"2024-02-15T17:00:00"

// ✅ 有时区 - 无歧义
"2024-02-15T17:00:00+01:00"

Performance Consideration

性能考量

Custom closures run for every date - optimize expensive operations:
swift
// ❌ 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
}

自定义闭包会为每个日期执行 - 优化昂贵的操作:
swift
// ❌ 为每个日期创建新的格式化器
decoder.dateDecodingStrategy = .custom { decoder in
    let formatter = DateFormatter()  // 开销大!
    // ...
}

// ✅ 复用格式化器
let sharedFormatter = DateFormatter()
sharedFormatter.dateFormat = "yyyy-MM-dd"

decoder.dateDecodingStrategy = .custom { decoder in
    // 使用sharedFormatter
}

Part 5: Type Transformation

第五部分:类型转换

StringBacked Wrapper

字符串包装器

Handle APIs that encode numbers as strings:
swift
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:
swift
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: "无法将'\(string)'转换为\(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)

Type Coercion

类型强制转换

For loosely typed APIs that may return different types:
swift
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.

处理可能返回不同类型的松散类型API:
swift
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: "无法将值解码为String、Int或Double"
            )
        }
    }
}
警告:除非API确实不可预测,否则避免使用此模式。优先使用严格类型。

Part 6: Advanced Patterns

第六部分:高级模式

DecodableWithConfiguration (iOS 15+)

DecodableWithConfiguration(iOS 15+)

For types that need data unavailable in JSON:
swift
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
)
适用于需要JSON中不存在的数据的类型:
swift
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
)

userInfo Workaround (iOS 15-16)

userInfo 兼容方案(iOS 15-16)

swift
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")!
swift
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")!

Partial Decoding

部分解码

Decode only the fields you need:
swift
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

仅解码你需要的字段:
swift
struct ArticlePreview: Decodable {
    let id: UUID
    let title: String
    // 忽略body、comments等字段
}

// JSON包含更多字段,但我们仅解码id和title

Part 7: Debugging

第七部分:调试

DecodingError Cases

DecodingError 类型

swift
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)")
}
swift
do {
    let user = try decoder.decode(User.self, from: data)
} catch DecodingError.keyNotFound(let key, let context) {
    print("路径\(context.codingPath)下缺少键'\(key)'")
} catch DecodingError.typeMismatch(let type, let context) {
    print("路径\(context.codingPath)下类型不匹配,预期类型为\(type)")
} catch DecodingError.valueNotFound(let type, let context) {
    print("路径\(context.codingPath)下未找到类型为\(type)的值")
} catch DecodingError.dataCorrupted(let context) {
    print("路径\(context.codingPath)下数据损坏")
} catch {
    print("其他错误: \(error)")
}

Debugging Techniques

调试技巧

1. Pretty-print JSON
swift
let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
let jsonData = try encoder.encode(user)
print(String(data: jsonData, encoding: .utf8)!)
2. Inspect coding path
swift
// In custom init(from:)
print("Decoding at path: \(decoder.codingPath)")
3. Validate JSON structure
swift
// Quick check: Can it decode as Any?
let json = try JSONSerialization.jsonObject(with: data)
print(json)  // See actual structure

1. 格式化输出JSON
swift
let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
let jsonData = try encoder.encode(user)
print(String(data: jsonData, encoding: .utf8)!)
2. 检查编码路径
swift
// 在自定义init(from:)中
print("当前解码路径: \(decoder.codingPath)")
3. 验证JSON结构
swift
// 快速检查:能否解码为Any?
let json = try JSONSerialization.jsonObject(with: data)
print(json)  // 查看实际结构

Anti-Patterns

反模式

Anti-PatternCostBetter Approach
Manual JSON string buildingInjection vulnerabilities, escaping bugs, no type safetyUse
JSONEncoder
try?
swallowing DecodingError
Silent failures, debugging nightmares, data lossHandle specific error cases
Optional properties to avoid decode errorsRuntime crashes, nil checks everywhere, masks structural issuesFix JSON/model mismatch or use
DecodableWithConfiguration
Duplicating partial models2-5 hours maintenance per change, sync issues, fragileUse bridge types or configuration
Ignoring date timezoneIntermittent bugs across regions, data corruptionAlways use ISO8601 with timezone or explicit UTC
JSONSerialization
for Codable types
3x more boilerplate, manual type casting, error-proneUse
JSONDecoder
/
JSONEncoder
No locale on DateFormatterParsing fails in non-US localesSet
locale = Locale(identifier: "en_US_POSIX")
反模式代价更好的方案
手动拼接JSON字符串注入漏洞、转义错误、无类型安全使用
JSONEncoder
try?
忽略DecodingError
静默失败、调试困难、数据丢失处理特定错误类型
将属性设为可选以避免解码错误运行时崩溃、处处需要nil检查、掩盖结构问题修复JSON/模型不匹配或使用
DecodableWithConfiguration
重复定义部分模型每次变更需2-5小时维护、同步问题、脆弱性使用桥接类型或配置
忽略日期时区跨地区间歇性bug、数据损坏始终使用带时区的ISO8601格式或显式UTC
对Codable类型使用
JSONSerialization
代码量是3倍、手动类型转换、易出错使用
JSONDecoder
/
JSONEncoder
不为DateFormatter设置locale非美国地区解析失败设置
locale = Locale(identifier: "en_US_POSIX")

Why try? is Dangerous

为什么
try?
很危险

swift
// ❌ 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
}

swift
// ❌ 静默失败 - 生产环境隐患
let user = try? JSONDecoder().decode(User.self, from: data)
// 如果解码失败,user为nil - 但你不知道原因

// ✅ 显式错误处理
do {
    let user = try JSONDecoder().decode(User.self, from: data)
} catch {
    logger.error("解码User失败: \(error)")
    // 现在你知道失败原因了
}

Pressure Scenarios

压力场景

Scenario 1: "Just Use try? to Make It Compile"

场景1:“用try?让代码编译通过就行”

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:
  • "It's only failing on 1% of requests"
  • "We can add logging later"
  • "Customers won't notice"
What Actually Happens:
  • Silent data loss for that 1%
  • No logs, so you can't debug in production
  • Customer complaints 3 months later
  • You've forgotten the context by then
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:
swift
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.

背景:API集成 deadline 是明天,解码器在某些边缘场景下失败。
压力:“我们之后再调试,先让它能运行起来。”
你可能会这样自我说服:
  • “只有1%的请求会失败”
  • “我们之后再加日志”
  • “用户不会注意到”
实际后果:
  • 那1%的请求会静默丢失数据
  • 没有日志,生产环境无法调试
  • 3个月后收到客户投诉
  • 你已经忘记当时的上下文
正确应对:
“用
try?
会导致数据静默丢失。给我5分钟处理特定错误类型。如果确实是罕见情况,我会添加日志,这样我们之后可以修复根本原因。”
5分钟修复:
swift
do {
    return try decoder.decode(User.self, from: data)
} catch DecodingError.keyNotFound(let key, let context) {
    logger.error("API响应中缺少键'\(key)'", metadata: [
        "path": .string(context.codingPath.description),
        "rawJSON": .string(String(data: data, encoding: .utf8) ?? "")
    ])
    throw APIError.invalidResponse(reason: "缺少键: \(key)")
} catch {
    logger.error("解码User失败", error: error)
    throw APIError.decodingFailed(error)
}
结果:你发现API在用户已删除时有时会省略
email
字段。修复方案:仅针对该场景将
email
设为可选,而非所有情况。

Scenario 2: "Dates Are Intermittent, Must Be Server Bug"

场景2:“日期解析时好时坏,肯定是服务器的问题”

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:
  • "My tests pass locally"
  • "The server is probably sending bad data"
  • "It's their device settings"
What Actually Happens:
  • Server sends dates without timezone:
    "2024-12-14T10:00:00"
  • Your device (PST) interprets as 10:00 PST
  • QA device (CET) interprets as 10:00 CET
  • Different absolute times, intermittent bugs
Discipline Response:
"Intermittent date failures are almost always timezone issues. Let me check if we're using ISO8601 with timezone offsets."
Check:
swift
// ❌ 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.

背景:日期解析在你的时区正常,但欧洲QA团队的测试失败。
压力:“我这里正常,QA肯定操作错了。”
你可能会这样自我说服:
  • “我的本地测试都通过了”
  • “肯定是服务器返回的数据有问题”
  • “是他们的设备设置问题”
实际后果:
  • 服务器返回的日期没有时区:
    "2024-12-14T10:00:00"
  • 你的设备(PST时区)解析为10:00 PST
  • QA的设备(CET时区)解析为10:00 CET
  • 实际时间不一致,出现间歇性bug
正确应对:
“日期解析的间歇性问题几乎都是时区导致的。让我检查我们是否使用了带时区偏移的ISO8601格式。”
修复方案:
swift
// ❌ 当前实现(跨时区失败)
decoder.dateDecodingStrategy = .iso8601

// 服务器返回: "2024-12-14T10:00:00"(无时区)
// PST设备: 12月14日 10:00 PST
// CET设备: 12月14日 10:00 CET
// Bug: 时间不一致!

// ✅ 修复:要求服务器返回带时区的日期
// "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: "无效的ISO8601日期: \(dateString)"
        )
    }

    return date
}
结果:Bug修复,服务器为API添加了时区(或者你显式按UTC解析)。不再有间歇性失败。

Scenario 3: "Just Make It Optional"

场景3:“把它设为可选就行”

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:
  • "It's faster than fixing the API"
  • "We can make it non-optional later"
  • "Users won't notice"
What Actually Happens:
  • Field is actually required for the feature
  • You add
    user.email ?? ""
    everywhere
  • 3 months later: production crash because
    email
    was nil
  • Now you can't remember why it was optional
Discipline 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:
swift
// 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:
  1. API is wrong: Field should be there → File bug, get hotfix
  2. Model is wrong: Field is optional in some flows → Use proper optionality with clear documentation
  3. Structural mismatch: Field is nested → Use nested container
Result: You discover
email
is nested in
user.contact.email
in the new API version. Fix with nested container, not optionality.
swift
// ✅ 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)
    }
}

背景:新的API字段导致解码失败。产品经理要求1小时内修复。
压力:“你不能把那个字段设为可选吗?我们需要尽快上线。”
你可能会这样自我说服:
  • “这比修复API快”
  • “我们之后再把它改回非可选”
  • “用户不会注意到”
实际后果:
  • 该字段实际上是功能必需的
  • 你在各处添加
    user.email ?? ""
  • 3个月后:生产环境崩溃,因为
    email
    为nil
  • 你已经忘记为什么它被设为可选
正确应对:
“把它设为可选会掩盖真正的问题。给我10分钟检查是API错误还是我们的模型错误。”
排查步骤:
swift
// 步骤1:打印原始JSON
do {
    let json = try JSONSerialization.jsonObject(with: data)
    print(json)
} catch {
    print("无效JSON: \(error)")
}

// 步骤2:检查键是否存在但值为null
// {"email": null} 与 键不存在的情况

// 步骤3:查看API文档 - email是否确实是必需的?
常见结论:
  1. API错误:字段应该存在 → 提交bug,获取热修复
  2. 模型错误:字段在某些流程中是可选的 → 合理使用可选性并添加清晰注释
  3. 结构不匹配:字段是嵌套的 → 使用嵌套容器
结果:你发现
email
在新API版本中嵌套在
user.contact.email
中。修复方案是使用嵌套容器,而非将字段设为可选。
swift
// ✅ 正确修复
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)
    }
}

Related Skills

相关技能

  • swift-concurrency — Codable types crossing actor boundaries must be
    Sendable
  • swiftdata
    @Model
    types use Codable for CloudKit sync
  • networking
    Coder
    protocol wraps Codable for Network.framework
  • app-intents-ref
    AppEnum
    parameters use Codable serialization

  • swift-concurrency — 跨actor边界的Codable类型必须遵循
    Sendable
  • swiftdata
    @Model
    类型使用Codable实现CloudKit同步
  • networking
    Coder
    协议为Network.framework封装Codable
  • app-intents-ref
    AppEnum
    参数使用Codable序列化

Key Takeaways

关键要点

  1. Prefer automatic synthesis — Add
    : Codable
    when structure matches JSON
  2. Use CodingKeys for simple mismatches — Rename or exclude without manual code
  3. Manual implementation for structural differences — Nested containers, bridge types
  4. Always set locale and timezone
    DateFormatter
    requires
    en_US_POSIX
    and explicit timezone
  5. Never swallow errors with try? — Handle
    DecodingError
    cases explicitly
  6. Codable + Sendable — Value types (structs/enums) are ideal for async networking
Core Principle: Codable is Swift's universal serialization protocol. Master it once, use it everywhere.
  1. 优先使用自动合成 — 当结构与JSON匹配时,只需添加
    : Codable
  2. 简单不匹配时使用CodingKeys — 无需手动代码即可重命名或排除属性
  3. 结构差异时手动实现 — 嵌套容器、桥接类型
  4. 始终设置locale和时区
    DateFormatter
    需要
    en_US_POSIX
    和显式时区
  5. 永远不要用try?忽略错误 — 显式处理
    DecodingError
    类型
  6. Codable + Sendable — 值类型(结构体/枚举)是异步网络请求的理想选择
核心原则:Codable是Swift的通用序列化协议。掌握一次,随处可用。