Swift Codable:如何将顶层数据编码至嵌套容器
解决Codable编码时合并顶层user与子对象数据的问题
我明白你的问题了——解码时你把通用的user字段从data字典里抽出来放到了顶层ApiResponse里,但编码时需要把user和URL专属的DataType内容合并回同一个data字典,现在的问题是两次编码操作互相覆盖了data字段。
问题根源
你当前的encode(to:)方法里,先调用baseContainer.encodeIfPresent(self.data, forKey: .data)把DataType的内容写入data字段,随后调用baseContainer.nestedContainer(keyedBy: DataDictKeys.self, forKey: .data)创建了一个新的嵌套容器,这会直接覆盖掉之前写入的DataType内容,最终data字典里只剩下user。
解决方案:合并编码到同一个容器
我们需要把DataType的所有键值对和user字段合并到同一个data字典容器里,而不是分开编码。这里可以通过动态CodingKey和**超级编码器(superEncoder)**来实现,无需额外序列化/反序列化操作。
修改后的完整代码
// MARK: - Common User struct User: Codable { var name: String? } // MARK: - Dynamic Coding Key(处理任意字符串键) private struct DynamicCodingKey: CodingKey { var stringValue: String var intValue: Int? init?(stringValue: String) { self.stringValue = stringValue self.intValue = nil } init?(intValue: Int) { self.stringValue = "\(intValue)" self.intValue = intValue } } // MARK: - Abstract Response struct ApiResponse<DataType: Codable>: Codable { // MARK: Properties var result: String var user: User? var data: DataType? // MARK: Coding Keys enum CodingKeys: String, CodingKey { case result, data } enum DataDictKeys: String, CodingKey { case user } // MARK: Decodable(保持你原来的解码逻辑不变) init(from decoder: Decoder) throws { let baseContainer = try decoder.container(keyedBy: CodingKeys.self) self.result = try baseContainer.decode(String.self, forKey: .result) self.data = try baseContainer.decodeIfPresent(DataType.self, forKey: .data) let dataContainer = try baseContainer.nestedContainer(keyedBy: DataDictKeys.self, forKey: .data) self.user = try dataContainer.decodeIfPresent(User.self, forKey: .user) } // MARK: Encodable(修改后的编码逻辑) func encode(to encoder: Encoder) throws { var baseContainer = encoder.container(keyedBy: CodingKeys.self) try baseContainer.encode(self.result, forKey: .result) // 创建一个动态键的嵌套容器,用于合并data和user var mergedDataContainer = baseContainer.nestedContainer(keyedBy: DynamicCodingKey.self, forKey: .data) // 1. 编码URL专属的DataType内容到合并容器 if let data = self.data { let dataEncoder = mergedDataContainer.superEncoder() try data.encode(to: dataEncoder) } // 2. 编码user到合并容器 if let user = self.user { let userKey = DynamicCodingKey(stringValue: "user")! let userEncoder = mergedDataContainer.superEncoder(forKey: userKey) try user.encode(to: userEncoder) } } } // MARK: - Concrete Response typealias OrderDataResponse = ApiResponse<OrderData> struct OrderData: Codable { var orderCount: Int = 0 enum CodingKeys: String, CodingKey { case orderCount = "order_count" } }
测试验证
用你提供的示例代码测试,现在编码后的plist会同时包含user和order_count:
let orderDataResponseJson = """ { "result":"OK", "data":{ "user":{ "name":"John" }, "order_count":10 } } """ // 解码JSON let jsonData = orderDataResponseJson.data(using: .utf8)! let response = try JSONDecoder().decode(OrderDataResponse.self, from: jsonData) // 编码为Plist let plistEncoder = PropertyListEncoder() plistEncoder.outputFormat = .xml let plistData = try plistEncoder.encode(response) let plistString = String(data: plistData, encoding: .utf8)! print(plistString)
输出的plist内容会包含order_count和user:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>data</key> <dict> <key>order_count</key> <integer>10</integer> <key>user</key> <dict> <key>name</key> <string>John</string> </dict> </dict> <key>result</key> <string>OK</string> </dict> </plist>
原理说明
DynamicCodingKey允许我们处理任意字符串键,适配DataType的所有自定义字段和user字段。superEncoder()会把DataType的所有键值对直接写入mergedDataContainer,而superEncoder(forKey:)会把user作为一个嵌套字典写入同一个容器,这样就实现了两者的合并,不会互相覆盖。
内容的提问来源于stack exchange,提问作者ABeard89
相关产品推荐
相关产品推荐

