iOS Codable 模型与严格解析
iOS Codable 模型与严格解析
Codable 能减少序列化样板代码,但模型是否可靠仍取决于接口契约。把所有字段声明为可选可以让解析“更容易成功”,却会把协议错误推迟到业务页面。更稳妥的做法是在网络边界严格解析,区分协议模型与领域模型,并只为接口确实允许的缺失情况设计可选值。
一、协议模型忠实表达响应
struct OrderDTO: Decodable {
let identifier: String
let amountInCent: Int64
let status: String
let createdAt: Date
enum CodingKeys: String, CodingKey {
case identifier = "orderId"
case amountInCent = "amount"
case status
case createdAt
}
}
字段被协议定义为必填时使用非可选类型,类型或字段缺失会直接导致解析失败。这样问题停留在网络边界,而不是进入页面后以空白内容表现。
二、DTO 与领域模型分离
接口状态字符串不应直接扩散到业务层:
enum OrderStatus: Equatable {
case pending
case completed
case cancelled
}
extension OrderDTO {
func toDomain() throws -> Order {
let domainStatus: OrderStatus
switch status {
case "pending":
domainStatus = .pending
case "completed":
domainStatus = .completed
case "cancelled":
domainStatus = .cancelled
default:
throw OrderMappingError.unsupportedStatus(status)
}
return Order(
id: identifier,
amountInCent: amountInCent,
status: domainStatus,
createdAt: createdAt
)
}
}
未知状态是否允许继续展示,由业务协议决定。关键流程不应擅自把未知值映射为某个正常状态。
三、统一日期解析策略
日期格式应在统一解码器中配置,不要让每个模型自行创建格式化器。日期格式化器创建和线程使用也应由网络基础层管理。
func makeDecoder(dateStrategy: JSONDecoder.DateDecodingStrategy) -> JSONDecoder {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = dateStrategy
return decoder
}
如果同一响应中存在多种日期格式,优先推动协议统一;确实无法统一时,再对特定字段实现明确解析。
四、区分 null 与字段缺失
默认可选属性通常会把空值和缺失都解码为 nil。若两者业务含义不同,需要自定义解码并检查键是否存在。
struct ProfilePatchDTO: Decodable {
enum IntroductionValue {
case unchanged
case cleared
case value(String)
}
let introduction: IntroductionValue
enum CodingKeys: String, CodingKey {
case introduction
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
guard container.contains(.introduction) else {
introduction = .unchanged
return
}
if try container.decodeNil(forKey: .introduction) {
introduction = .cleared
} else {
introduction = .value(
try container.decode(String.self, forKey: .introduction)
)
}
}
}
只有协议明确区分这些含义时才需要增加复杂度。
五、处理多态对象
消息列表可能包含文本、图片和系统事件。可以先读取类型字段,再选择具体模型:
enum MessageDTO: Decodable {
case text(TextMessageDTO)
case system(SystemMessageDTO)
private enum CodingKeys: String, CodingKey {
case type
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let type = try container.decode(String.self, forKey: .type)
switch type {
case "text":
self = .text(try TextMessageDTO(from: decoder))
case "system":
self = .system(try SystemMessageDTO(from: decoder))
default:
throw DecodingError.dataCorruptedError(
forKey: .type,
in: container,
debugDescription: "不支持的消息类型"
)
}
}
}
若产品允许忽略未知消息,应在列表解析层明确记录并过滤,不能让每个页面随意决定。
六、不要默认做宽松数组解析
某个数组元素解析失败时跳过它,可以让页面继续展示,但也可能隐藏关键数据缺失。资讯推荐可能允许忽略单项,交易记录则通常要求整体失败并报警。
宽松解析必须是业务策略,并记录足够的非敏感诊断信息,不应成为通用解码器默认行为。
七、数值类型要符合范围
金额优先使用最小单位整数或明确十进制类型,不用二进制浮点直接承载精确金额。标识即使全是数字,也常应使用字符串,避免前导零和范围问题。
接口可能返回超出 Int 范围的值时,应选择明确位宽并测试边界。
八、编码请求也要有专用模型
更新命令和响应实体字段通常不同。请求模型只包含允许提交的字段,避免把只读标识或界面临时值一起编码。
struct UpdateProfileCommand: Encodable {
let nickname: String
let introduction: String?
}
是否编码空值、缺失字段的含义必须遵循协议,不要在全局编码器中随意删除所有空值。
九、记录错误而不泄露数据
解析失败日志可以包含模型类型、字段路径和请求标识,但不应完整输出用户响应。错误上下文足以定位协议变化时,无需记录敏感内容。
十、建立契约样本测试
为成功、空值、缺失字段、未知枚举、数值边界和多态类型准备固定样本。测试 DTO 解码和领域映射分别失败在预期位置。
协议升级时更新样本,能够提前发现字段类型和必填性变化。
总结
可靠的 Codable 设计强调边界严格。DTO 忠实表达协议,领域映射收敛字符串和状态,日期与数值使用统一策略,并只在业务明确允许时宽松处理。这样解析错误能尽早暴露,也不会让大量可选值污染整个应用。
- 点赞
- 收藏
- 关注作者
评论(0)