Loading...
Loading...
Compare original and translation side by side
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? → 手动实现 + 嵌套容器| 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 |
// ✅ Automatic synthesis
struct User: Codable {
let id: UUID // Codable
var name: String // Codable
var membershipPoints: Int // Codable
}
// JSON: {"id":"...", "name":"Alice", "membershipPoints":100}// ✅ 自动合成
struct User: Codable {
let id: UUID // 遵循Codable
var name: String // 遵循Codable
var membershipPoints: Int // 遵循Codable
}
// JSON: {"id":"...", "name":"Alice", "membershipPoints":100}enum Direction: String, Codable {
case north, south, east, west
}
// Encodes as: "north"enum Direction: String, Codable {
case north, south, east, west
}
// 编码结果: "north"enum Status: Codable {
case success
case failure
case pending
}
// Encodes as: {"success":{}}enum Status: Codable {
case success
case failure
case pending
}
// 编码结果: {"success":{}}enum APIResult: Codable {
case success(data: String, count: Int)
case error(code: Int, message: String)
}
// success case encodes as:
// {"success":{"data":"example","count":5}}_0_1enum Command: Codable {
case store(String, Int) // ❌ Unlabeled
}
// Encodes as: {"store":{"_0":"value","_1":42}}enum Command: Codable {
case store(key: String, value: Int) // ✅ Labeled
}
// Encodes as: {"store":{"key":"value","value":42}}enum APIResult: Codable {
case success(data: String, count: Int)
case error(code: Int, message: String)
}
// success case编码结果:
// {"success":{"data":"example","count":5}}_0_1enum Command: Codable {
case store(String, Int) // ❌ 未标记
}
// 编码结果: {"store":{"_0":"value","_1":42}}enum Command: Codable {
case store(key: String, value: Int) // ✅ 已标记
}
// 编码结果: {"store":{"key":"value","value":42}}@Published@State@AppStorageinit(from:)@Published@State@AppStorageinit(from:)CodingKeysCodingKeysstruct 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":"..."}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":"..."}CodingKeysstruct 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
}
}init(from:)CodingKeysstruct NoteCollection: Codable {
let name: String
let notes: [Note]
var localDrafts: [Note] = [] // ✅ 必须提供默认值
enum CodingKeys: CodingKey {
case name
case notes
// 省略localDrafts - 不会被编解码
}
}init(from:)let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
// JSON: {"first_name":"Alice", "last_name":"Smith"}
// Decodes to: User(firstName: "Alice", lastName: "Smith")let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
// JSON: {"first_name":"Alice", "last_name":"Smith"}
// 解码结果: User(firstName: "Alice", lastName: "Smith"){CaseName}CodingKeysenum 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}}{CaseName}CodingKeys{CaseName}CodingKeysenum 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}}CodingKeys{CaseName}CodingKeysinit(from:)encode(to:)init(from:)encode(to:)| 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结构 |
// 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:
// {
// "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)
}
}// 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: {"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
}
}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)"
)
}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)"
)
}2024-02-15T17:00:00+01:00// ❌ 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// ❌ 无时区 - 解析结果依赖设备区域设置
"2024-02-15T17:00:00"
// ✅ 有时区 - 无歧义
"2024-02-15T17:00:00+01:00"// ❌ 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
}// ❌ 为每个日期创建新的格式化器
decoder.dateDecodingStrategy = .custom { decoder in
let formatter = DateFormatter() // 开销大!
// ...
}
// ✅ 复用格式化器
let sharedFormatter = DateFormatter()
sharedFormatter.dateFormat = "yyyy-MM-dd"
decoder.dateDecodingStrategy = .custom { decoder in
// 使用sharedFormatter
}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)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)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"
)
}
}
}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"
)
}
}
}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
)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
)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")!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")!struct ArticlePreview: Decodable {
let id: UUID
let title: String
// Omit body, comments, etc.
}
// JSON has many more fields, but we only decode id and titlestruct ArticlePreview: Decodable {
let id: UUID
let title: String
// 忽略body、comments等字段
}
// JSON包含更多字段,但我们仅解码id和titledo {
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)")
}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)")
}let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
let jsonData = try encoder.encode(user)
print(String(data: jsonData, encoding: .utf8)!)// In custom init(from:)
print("Decoding at path: \(decoder.codingPath)")// Quick check: Can it decode as Any?
let json = try JSONSerialization.jsonObject(with: data)
print(json) // See actual structurelet encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
let jsonData = try encoder.encode(user)
print(String(data: jsonData, encoding: .utf8)!)// 在自定义init(from:)中
print("当前解码路径: \(decoder.codingPath)")// 快速检查:能否解码为Any?
let json = try JSONSerialization.jsonObject(with: data)
print(json) // 查看实际结构| 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 | 非美国地区解析失败 | 设置 |
try?// ❌ 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
}// ❌ 静默失败 - 生产环境隐患
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)")
// 现在你知道失败原因了
}"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?
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)
}emailemail“用会导致数据静默丢失。给我5分钟处理特定错误类型。如果确实是罕见情况,我会添加日志,这样我们之后可以修复根本原因。”try?
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)
}emailemail"2024-12-14T10:00:00""Intermittent date failures are almost always timezone issues. Let me check if we're using ISO8601 with timezone offsets."
// ❌ 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
}"2024-12-14T10:00:00"“日期解析的间歇性问题几乎都是时区导致的。让我检查我们是否使用了带时区偏移的ISO8601格式。”
// ❌ 当前实现(跨时区失败)
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
}user.email ?? ""email"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."
// 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?emailuser.contact.email// ✅ 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)
}
}user.email ?? ""email“把它设为可选会掩盖真正的问题。给我10分钟检查是API错误还是我们的模型错误。”
// 步骤1:打印原始JSON
do {
let json = try JSONSerialization.jsonObject(with: data)
print(json)
} catch {
print("无效JSON: \(error)")
}
// 步骤2:检查键是否存在但值为null
// {"email": null} 与 键不存在的情况
// 步骤3:查看API文档 - email是否确实是必需的?emailuser.contact.email// ✅ 正确修复
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)
}
}Sendable@ModelCoderAppEnumSendable@ModelCoderAppEnum: CodableDateFormatteren_US_POSIXDecodingError: CodableDateFormatteren_US_POSIXDecodingError