You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何让带泛型属性的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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.10 19:20:30