如何让带泛型属性的Swift Codable分页结构体适配动态Payload Key?
解决Swift Codable泛型适配动态Payload Key的分页API响应问题
要让Paginable结构体适配API返回的动态Payload Key,核心思路是让解码逻辑能动态获取对应Body类型的JSON键名,下面提供两种实用的实现方案:
方案一:让Body类型遵循自定义协议暴露键名
这种方式让Body类型自己声明对应的JSON键名,代码结构更清晰,适合长期维护。
步骤1:定义协议
先创建一个协议,要求实现类提供静态的Payload键名字段:
public protocol PayloadKeyProviding { static var payloadKey: String { get } }
步骤2:修改Paginable结构体
自定义解码/编码逻辑,从协议中获取动态键名,同时处理固定的nextCursor字段:
public struct Paginable<Body>: Codable where Body: Codable & PayloadKeyProviding { public let body: Body public let cursor: String? // 自定义解码逻辑 public init(from decoder: Decoder) throws { // 解析固定的nextCursor字段 let cursorContainer = try decoder.container(keyedBy: CodingKeys.self) cursor = try cursorContainer.decodeIfPresent(String.self, forKey: .cursor) // 获取Body对应的动态键名,解析Payload内容 let payloadKey = Body.payloadKey let payloadContainer = try decoder.container(keyedBy: DynamicCodingKey.self) guard let dynamicKey = DynamicCodingKey(stringValue: payloadKey) else { throw DecodingError.dataCorrupted(.init(codingPath: [], debugDescription: "无效的Payload键名")) } body = try payloadContainer.decode(Body.self, forKey: dynamicKey) } // 自定义编码逻辑(如果需要编码分页请求/响应) public func encode(to encoder: Encoder) throws { var cursorContainer = encoder.container(keyedBy: CodingKeys.self) try cursorContainer.encodeIfPresent(cursor, forKey: .cursor) let payloadKey = Body.payloadKey var payloadContainer = encoder.container(keyedBy: DynamicCodingKey.self) guard let dynamicKey = DynamicCodingKey(stringValue: payloadKey) else { throw EncodingError.invalidValue(payloadKey, .init(codingPath: [], debugDescription: "无效的Payload键名")) } try payloadContainer.encode(body, forKey: dynamicKey) } // 固定的cursor键映射 private enum CodingKeys: String, CodingKey { case cursor = "nextCursor" } // 动态CodingKey实现,用于解析任意字符串键 private struct DynamicCodingKey: CodingKey { let stringValue: String init?(stringValue: String) { self.stringValue = stringValue } let intValue: Int? = nil init?(intValue: Int) { return nil } } }
步骤3:给Body类型扩展协议
针对不同的Body类型,扩展实现PayloadKeyProviding协议,指定对应的JSON键名:
// 示例1:用户数组对应的键是"users" struct User: Codable { let id: String let name: String } extension Array: PayloadKeyProviding where Element == User { static var payloadKey: String { "users" } } // 示例2:单个大对象对应的键是"item" struct LargeItem: Codable, PayloadKeyProviding { static var payloadKey: String { "item" } let id: String let content: String }
使用示例
// 解码用户列表响应 let usersJson = """ { "nextCursor": "abc123", "users": [{"id": "1", "name": "Alice"}, {"id": "2", "name": "Bob"}] } """.data(using: .utf8)! let paginatedUsers = try JSONDecoder().decode(Paginable<[User]>.self, from: usersJson) print(paginatedUsers.body) // 输出用户数组 print(paginatedUsers.cursor) // 输出Optional("abc123") // 解码单个大对象响应 let itemJson = """ { "nextCursor": "xyz789", "item": {"id": "obj1", "content": "Some large content"} } """.data(using: .utf8)! let paginatedItem = try JSONDecoder().decode(Paginable<LargeItem>.self, from: itemJson) print(paginatedItem.body.content) // 输出"Some large content"
方案二:通过泛型参数传递键名(无需修改Body类型)
如果不想给Body类型添加额外协议,可以将Payload Key作为泛型参数传入,灵活性更高:
实现代码
public struct Paginable<Body, Key: CodingKey>: Codable where Body: Codable { public let body: Body public let cursor: String? public init(from decoder: Decoder) throws { // 解析固定的nextCursor let cursorContainer = try decoder.container(keyedBy: CodingKeys.self) cursor = try cursorContainer.decodeIfPresent(String.self, forKey: .cursor) // 通过泛型Key解析Payload let payloadContainer = try decoder.container(keyedBy: Key.self) body = try payloadContainer.decode(Body.self, forKey: Key(stringValue: Key.stringValue)!) } public func encode(to encoder: Encoder) throws { var cursorContainer = encoder.container(keyedBy: CodingKeys.self) try cursorContainer.encodeIfPresent(cursor, forKey: .cursor) var payloadContainer = encoder.container(keyedBy: Key.self) try payloadContainer.encode(body, forKey: Key(stringValue: Key.stringValue)!) } private enum CodingKeys: String, CodingKey { case cursor = "nextCursor" } } // 定义具体的键类型 struct UsersKey: CodingKey { static let stringValue = "users" let stringValue: String = UsersKey.stringValue init?(stringValue: String) { guard stringValue == UsersKey.stringValue else { return nil } self.init() } let intValue: Int? = nil init?(intValue: Int) { return nil } init() {} } struct ItemKey: CodingKey { static let stringValue = "item" let stringValue: String = ItemKey.stringValue init?(stringValue: String) { guard stringValue == ItemKey.stringValue else { return nil } self.init() } let intValue: Int? = nil init?(intValue: Int) { return nil } init() {} }
使用示例
let paginatedUsers = try JSONDecoder().decode(Paginable<[User], UsersKey>.self, from: usersJson) let paginatedItem = try JSONDecoder().decode(Paginable<LargeItem, ItemKey>.self, from: itemJson)
方案对比
- 方案一:代码更简洁,Body类型自管理键名,适合项目中固定的API响应格式。
- 方案二:无需修改原有Body类型,灵活性更高,适合临时或多样化的键名场景。
内容的提问来源于stack exchange,提问作者Majster
相关产品推荐
相关产品推荐

