如何解码同时包含预期CodingKeys的动态根键?
Swift JSON解码:同时处理固定键与动态键
问题场景
需要解码包含固定键和动态键的JSON数据,示例JSON如下:
{ "name": "", "age": null, "street": "", "appended/1": null, "appended/2": null, "appended/3": null }
其中name、age、street是固定键,对应结构体的已知属性;appended/1、appended/2这类动态键数量不固定,需要批量读取并处理。
尝试过的方法及问题
仅用固定CodingKeys解码
常规的init(from:)实现只能读取预定义CodingKeys对应的字段,无法处理动态键:init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) id = try container.decode(Int.self, forKey: .id) name = try container.decodeIfPresent(String.self, forKey: .name) ?? "" // 无法处理动态键 }先处理动态键再处理固定键
使用动态CodingKey容器遍历处理动态键后,无法再读取固定键——因为解码器的读取状态被消耗,后续获取固定键容器会失败:init(from decoder: Decoder) throws { let dynamicKeysContainer = try decoder.container(keyedBy: DynamicCodingKey.self) var appendedFound: [Appended] = [] try dynamicKeysContainer.allKeys.forEach { key in if key.stringValue.starts(with: "appended/"), let appendedNumber = Int(key.stringValue.split(separator: "/")[1]) { appendedFound.append(try dynamicKeysContainer.decode(Appended.self, forKey: key)) } } // 此处获取固定键容器会失败,解码器已被消耗 let container = try decoder.container(keyedBy: CodingKeys.self) id = try container.decode(Int.self, forKey: .id) name = try container.decodeIfPresent(String.self, forKey: .name) ?? "" appended = appendedFound }
解决方案
方案1:基于动态CodingKey容器统一处理
不需要分别创建多个容器,直接用动态CodingKey容器读取所有字段,包括固定键:
// 定义动态CodingKey struct DynamicCodingKey: CodingKey { var stringValue: String init?(stringValue: String) { self.stringValue = stringValue } var intValue: Int? init?(intValue: Int) { self.stringValue = "\(intValue)" self.intValue = intValue } } // 目标结构体 struct User: Decodable { var id: Int var name: String var age: Int? var street: String var appended: [Appended] enum CodingKeys: String, CodingKey { case id, name, age, street } init(from decoder: Decoder) throws { let dynamicContainer = try decoder.container(keyedBy: DynamicCodingKey.self) // 读取固定键字段 id = try dynamicContainer.decode(Int.self, forKey: DynamicCodingKey(stringValue: CodingKeys.id.rawValue)!) name = try dynamicContainer.decodeIfPresent(String.self, forKey: DynamicCodingKey(stringValue: CodingKeys.name.rawValue)!) ?? "" age = try dynamicContainer.decodeIfPresent(Int.self, forKey: DynamicCodingKey(stringValue: CodingKeys.age.rawValue)!) street = try dynamicContainer.decodeIfPresent(String.self, forKey: DynamicCodingKey(stringValue: CodingKeys.street.rawValue)!) ?? "" // 处理动态键 var appendedItems: [Appended] = [] try dynamicContainer.allKeys.forEach { key in guard key.stringValue.starts(with: "appended/"), let _ = Int(key.stringValue.split(separator: "/")[1]) else { return } let item = try dynamicContainer.decode(Appended.self, forKey: key) appendedItems.append(item) } appended = appendedItems } }
方案2:先解码为字典再拆分处理
如果动态键格式复杂,可先将整个JSON解码为字典,再从中提取固定字段和动态字段:
struct User: Decodable { var id: Int var name: String var age: Int? var street: String var appended: [Appended] init(from decoder: Decoder) throws { // 先解码为字典 let dictionary = try decoder.singleValueContainer().decode([String: Any].self) // 提取固定字段 id = dictionary["id"] as! Int name = dictionary["name"] as? String ?? "" age = dictionary["age"] as? Int street = dictionary["street"] as? String ?? "" // 处理动态键 var appendedItems: [Appended] = [] let jsonDecoder = JSONDecoder() for (key, value) in dictionary { guard key.starts(with: "appended/"), let _ = Int(key.split(separator: "/")[1]), let valueData = try? JSONSerialization.data(withJSONObject: value) else { continue } let item = try jsonDecoder.decode(Appended.self, from: valueData) appendedItems.append(item) } appended = appendedItems } }
关键说明
- 避免多次调用
decoder.container(keyedBy:)创建不同类型的容器,这会导致解码器状态异常,无法重复读取数据; - 方案1更贴合Swift Codable的原生设计,解码性能更优;方案2灵活性更强,适合动态键格式复杂多变的场景。
内容的提问来源于stack exchange,提问作者Learn2Code
相关产品推荐
相关产品推荐

