如何用Codable解码含动态对象的嵌套JSON键
处理动态键和多类型值的Codable字典方案
我之前也碰到过一模一样的问题——动态键、值类型还不固定的字典要实现Codable,确实头疼,因为[String:Any]没法直接用,Any不遵循Codable协议。给你两个亲测有效的方案,你可以根据场景选:
方案一:用枚举封装所有可能的值类型(类型安全首选)
先定义一个枚举,把所有可能的值类型都包含进去,让它自己实现Codable协议,这样就能和字典结合使用了:
enum DynamicValue: Codable { case string(String) case stringArray([String]) case intArray([Int]) init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() // 依次尝试解码不同类型,匹配到就赋值 if let singleString = try? container.decode(String.self) { self = .string(singleString) } else if let stringList = try? container.decode([String].self) { self = .stringArray(stringList) } else if let intList = try? container.decode([Int].self) { self = .intArray(intList) } else { throw DecodingError.dataCorruptedError( in: container, debugDescription: "Unsupported value type for DynamicValue" ) } } func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() // 编码时根据枚举类型对应处理 switch self { case .string(let value): try container.encode(value) case .stringArray(let value): try container.encode(value) case .intArray(let value): try container.encode(value) } } }
然后你的模型就可以这样写:
struct YourResponseModel: Codable { let testMap: [String: DynamicValue] // 其他字段... }
使用的时候,通过switch就能安全地取出对应类型的值,完全不用怕类型转换错误:
switch response.testMap["someKey"] { case .string(let str): print("Got string: \(str)") case .stringArray(let arr): print("Got string array: \(arr)") case .intArray(let arr): print("Got int array: \(arr)") case nil: print("Key not found") }
这个方案的好处是类型绝对安全,编译期就能帮你检查错误,后续维护也清晰。如果以后新增了值类型,只要在枚举里加个case就行。
方案二:手动实现Codable,直接用[String:Any](灵活但需注意类型安全)
如果一定要用[String:Any],可以手动实现init(from:)和encode(to:)方法,配合动态CodingKey来遍历处理每个键值对:
首先定义一个动态CodingKey的结构体:
struct DynamicCodingKey: CodingKey { var stringValue: String init?(stringValue: String) { self.stringValue = stringValue } var intValue: Int? init?(intValue: Int) { return nil // 我们只处理字符串键,这里返回nil就行 } }
然后在你的模型里手动实现Codable:
struct YourResponseModel: Codable { let testMap: [String: Any] enum CodingKeys: String, CodingKey { case testMap } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) // 获取testMap对应的嵌套容器 let testMapContainer = try container.nestedContainer(keyedBy: DynamicCodingKey.self, forKey: .testMap) var tempMap = [String: Any]() // 遍历所有动态键 for key in testMapContainer.allKeys { // 依次尝试解码不同类型 if let strValue = try? testMapContainer.decode(String.self, forKey: key) { tempMap[key.stringValue] = strValue } else if let strArray = try? testMapContainer.decode([String].self, forKey: key) { tempMap[key.stringValue] = strArray } else if let intArray = try? testMapContainer.decode([Int].self, forKey: key) { tempMap[key.stringValue] = intArray } // 要是有其他类型,在这里继续加判断就行 } testMap = tempMap } func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) var testMapContainer = container.nestedContainer(keyedBy: DynamicCodingKey.self, forKey: .testMap) for (key, value) in testMap { guard let codingKey = DynamicCodingKey(stringValue: key) else { continue } // 编码时根据值类型处理 if let strValue = value as? String { try testMapContainer.encode(strValue, forKey: codingKey) } else if let strArray = value as? [String] { try testMapContainer.encode(strArray, forKey: codingKey) } else if let intArray = value as? [Int] { try testMapContainer.encode(intArray, forKey: codingKey) } else { throw EncodingError.invalidValue( value, EncodingError.Context( codingPath: [codingKey], debugDescription: "Unsupported value type for encoding" ) ) } } } }
这个方案的好处是不需要额外的枚举类型,直接用[String:Any],但使用的时候你得自己做类型判断,容易出现类型转换失败的情况,适合对灵活性要求高、且能确保值类型范围的场景。
注意事项
- 如果后续有新的值类型(比如布尔值、数字),记得在对应的解码/编码逻辑里添加处理;
- 解码时如果遇到未知类型,最好抛出明确的错误,方便调试,不要默默忽略;
- 优先推荐方案一,类型安全带来的维护性提升真的很有用。
内容的提问来源于stack exchange,提问作者Kunal Kumar
相关产品推荐
相关产品推荐

