axiom-codable
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseSwift 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
常见问题与解决方案
| Error | Solution |
|---|---|
| "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 , keys:
_0_1swift
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_1swift
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:
- Computed properties - Only stored properties are encoded
- Non-Codable properties - Custom types without Codable conformance
- Property wrappers - ,
@Published(except@Statewith Codable types)@AppStorage - Class inheritance - Subclasses must implement manually
init(from:)
自动合成会在以下情况失效:
- 计算属性 - 只有存储属性会被编码
- 非Codable属性 - 未遵循Codable的自定义类型
- 属性包装器 - 、
@Published(@State搭配Codable类型除外)@AppStorage - 类继承 - 子类必须手动实现
init(from:)
Part 2: CodingKeys Customization
第二部分:自定义CodingKeys
Use enum to customize encoding/decoding without full manual implementation.
CodingKeys使用枚举在无需完全手动实现的情况下自定义编解码逻辑。
CodingKeysRenaming 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 to exclude them from encoding/decoding:
CodingKeysswift
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 manually.
init(from:)在中省略属性,即可将其排除在编解码流程之外:
CodingKeysswift
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}CodingKeysswift
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: with capitalized case name.
{CaseName}CodingKeys使用自定义枚举关联值的键:
{CaseName}CodingKeysswift
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}CodingKeysPart 3: Manual Implementation
第三部分:手动实现
For structural differences between JSON and Swift models, implement and .
init(from:)encode(to:)当JSON与Swift模型存在结构差异时,实现和方法。
init(from:)encode(to:)Container Types
容器类型
| Container | When to Use |
|---|---|
| Keyed | Dictionary-like data with string keys |
| Unkeyed | Array-like sequential data |
| Single-value | Wrapper types that encode as a single value |
| Nested | Hierarchical 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:
Timezone required: Without timezone offset, decoding may fail across regions
2024-02-15T17:00:00+01:00swift
// ❌ 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:00swift
// ❌ 无时区 - 解析结果依赖设备区域设置
"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和titlePart 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 structure1. 格式化输出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-Pattern | Cost | Better Approach |
|---|---|---|
| Manual JSON string building | Injection vulnerabilities, escaping bugs, no type safety | Use |
| Silent failures, debugging nightmares, data loss | Handle specific error cases |
| Optional properties to avoid decode errors | Runtime crashes, nil checks everywhere, masks structural issues | Fix JSON/model mismatch or use |
| Duplicating partial models | 2-5 hours maintenance per change, sync issues, fragile | Use bridge types or configuration |
| Ignoring date timezone | Intermittent bugs across regions, data corruption | Always use ISO8601 with timezone or explicit UTC |
| 3x more boilerplate, manual type casting, error-prone | Use |
| No locale on DateFormatter | Parsing fails in non-US locales | Set |
| 反模式 | 代价 | 更好的方案 |
|---|---|---|
| 手动拼接JSON字符串 | 注入漏洞、转义错误、无类型安全 | 使用 |
用 | 静默失败、调试困难、数据丢失 | 处理特定错误类型 |
| 将属性设为可选以避免解码错误 | 运行时崩溃、处处需要nil检查、掩盖结构问题 | 修复JSON/模型不匹配或使用 |
| 重复定义部分模型 | 每次变更需2-5小时维护、同步问题、脆弱性 | 使用桥接类型或配置 |
| 忽略日期时区 | 跨地区间歇性bug、数据损坏 | 始终使用带时区的ISO8601格式或显式UTC |
对Codable类型使用 | 代码量是3倍、手动类型转换、易出错 | 使用 |
| 不为DateFormatter设置locale | 非美国地区解析失败 | 设置 |
Why try? is Dangerous
为什么try?
很危险
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:
"Usinghere 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."try?
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 field for deleted users. Fix: make optional only for that case, not all users.
emailemail背景:API集成 deadline 是明天,解码器在某些边缘场景下失败。
压力:“我们之后再调试,先让它能运行起来。”
你可能会这样自我说服:
- “只有1%的请求会失败”
- “我们之后再加日志”
- “用户不会注意到”
实际后果:
- 那1%的请求会静默丢失数据
- 没有日志,生产环境无法调试
- 3个月后收到客户投诉
- 你已经忘记当时的上下文
正确应对:
“用会导致数据静默丢失。给我5分钟处理特定错误类型。如果确实是罕见情况,我会添加日志,这样我们之后可以修复根本原因。”try?
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在用户已删除时有时会省略字段。修复方案:仅针对该场景将设为可选,而非所有情况。
emailemailScenario 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 everywhere
user.email ?? "" - 3 months later: production crash because was nil
email - 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:
- API is wrong: Field should be there → File bug, get hotfix
- Model is wrong: Field is optional in some flows → Use proper optionality with clear documentation
- Structural mismatch: Field is nested → Use nested container
Result: You discover is nested in in the new API version. Fix with nested container, not optionality.
emailuser.contact.emailswift
// ✅ 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个月后:生产环境崩溃,因为为nil
email - 你已经忘记为什么它被设为可选
正确应对:
“把它设为可选会掩盖真正的问题。给我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是否确实是必需的?常见结论:
- API错误:字段应该存在 → 提交bug,获取热修复
- 模型错误:字段在某些流程中是可选的 → 合理使用可选性并添加清晰注释
- 结构不匹配:字段是嵌套的 → 使用嵌套容器
结果:你发现在新API版本中嵌套在中。修复方案是使用嵌套容器,而非将字段设为可选。
emailuser.contact.emailswift
// ✅ 正确修复
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 — types use Codable for CloudKit sync
@Model - networking — protocol wraps Codable for Network.framework
Coder - app-intents-ref — parameters use Codable serialization
AppEnum
- swift-concurrency — 跨actor边界的Codable类型必须遵循
Sendable - swiftdata — 类型使用Codable实现CloudKit同步
@Model - networking — 协议为Network.framework封装Codable
Coder - app-intents-ref — 参数使用Codable序列化
AppEnum
Key Takeaways
关键要点
- Prefer automatic synthesis — Add when structure matches JSON
: Codable - Use CodingKeys for simple mismatches — Rename or exclude without manual code
- Manual implementation for structural differences — Nested containers, bridge types
- Always set locale and timezone — requires
DateFormatterand explicit timezoneen_US_POSIX - Never swallow errors with try? — Handle cases explicitly
DecodingError - 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.
- 优先使用自动合成 — 当结构与JSON匹配时,只需添加
: Codable - 简单不匹配时使用CodingKeys — 无需手动代码即可重命名或排除属性
- 结构差异时手动实现 — 嵌套容器、桥接类型
- 始终设置locale和时区 — 需要
DateFormatter和显式时区en_US_POSIX - 永远不要用try?忽略错误 — 显式处理类型
DecodingError - Codable + Sendable — 值类型(结构体/枚举)是异步网络请求的理想选择
核心原则:Codable是Swift的通用序列化协议。掌握一次,随处可用。