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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.28 09:25:16