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

如何解码同时包含预期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这类动态键数量不固定,需要批量读取并处理。

尝试过的方法及问题

  1. 仅用固定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) ?? ""
        // 无法处理动态键
    }
    
  2. 先处理动态键再处理固定键
    使用动态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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.28 00:55:07