如何通过Codable协议结合JSONDecoder解析多结构体JSON数据?
解决Swift中遵循Codable协议的多结构体解码问题
首先得明确一个关键点:你不能直接把JSON解码到Animal协议类型,因为Codable需要具体的类型来实例化对象,而协议本身是抽象的,没有对应的初始化逻辑。不过我们可以通过两种常见的方式来解决这个问题,下面一步步来讲解。
方法一:用枚举作为包装类型(推荐)
这种方法最清晰可靠,我们可以创建一个枚举来包裹所有遵循Animal的结构体,让这个枚举实现Codable,并在解码时判断具体的类型。
步骤1:给JSON添加类型标识(可选但推荐)
为了明确区分是Dog还是Cat,最好在每个JSON payload里加一个类型字段,比如"kind": "dog"或者"kind": "cat"。修改后的JSON如下:
Dog的JSON:
{ "kind": "dog", "name": "fleabag", "age": 3, "type": "big" }Cat的JSON:
{ "kind": "cat", "name": "felix", "age": 2, "color": "black" }
步骤2:实现包装枚举
protocol Animal: Codable { var name: String { get } var age: Int { get } } struct Dog: Animal { let name: String let age: Int let type: String } struct Cat: Animal { let name: String let age: Int let color: String } // 包装枚举,处理解码逻辑 enum AnimalWrapper: Codable { case dog(Dog) case cat(Cat) // 定义编码解码的key private enum CodingKeys: String, CodingKey { case kind } // 枚举类型,对应JSON里的kind字段值 private enum AnimalType: String, Codable { case dog, cat } // 自定义解码逻辑 init(from decoder: Decoder) throws { // 先解析kind字段判断类型 let keyedContainer = try decoder.container(keyedBy: CodingKeys.self) let animalType = try keyedContainer.decode(AnimalType.self, forKey: .kind) // 再解析对应的结构体 let singleContainer = try decoder.singleValueContainer() switch animalType { case .dog: self = .dog(try singleContainer.decode(Dog.self)) case .cat: self = .cat(try singleContainer.decode(Cat.self)) } } // 自定义编码逻辑(如果需要编码的话) func encode(to encoder: Encoder) throws { var keyedContainer = encoder.container(keyedBy: CodingKeys.self) switch self { case .dog(let dog): try keyedContainer.encode(AnimalType.dog, forKey: .kind) try dog.encode(to: encoder) case .cat(let cat): try keyedContainer.encode(AnimalType.cat, forKey: .kind) try cat.encode(to: encoder) } } }
步骤3:使用JSONDecoder解码
// 解码Dog的示例 let dogJsonData = """ { "kind": "dog", "name": "fleabag", "age": 3, "type": "big" } """.data(using: .utf8)! do { let animalWrapper = try JSONDecoder().decode(AnimalWrapper.self, from: dogJsonData) switch animalWrapper { case .dog(let dog): print("解码出Dog:名字\(dog.name),年龄\(dog.age),品种\(dog.type)") case .cat(let cat): print("解码出Cat:名字\(cat.name),年龄\(cat.age),颜色\(cat.color)") } } catch { print("解码失败:\(error.localizedDescription)") } // 解码Cat的示例 let catJsonData = """ { "kind": "cat", "name": "felix", "age": 2, "color": "black" } """.data(using: .utf8)! do { let animalWrapper = try JSONDecoder().decode(AnimalWrapper.self, from: catJsonData) switch animalWrapper { case .dog(let dog): print("解码出Dog:名字\(dog.name),年龄\(dog.age),品种\(dog.type)") case .cat(let cat): print("解码出Cat:名字\(cat.name),年龄\(cat.age),颜色\(cat.color)") } } catch { print("解码失败:\(error.localizedDescription)") }
方法二:通过字段判断类型(无需添加kind字段)
如果不想修改原有的JSON,也可以通过判断JSON中是否存在type(Dog的专属字段)或color(Cat的专属字段)来区分类型。这种方法需要借助动态CodingKey:
实现代码
// 定义动态CodingKey,用于检查字段是否存在 struct DynamicCodingKey: CodingKey { let stringValue: String init?(stringValue: String) { self.stringValue = stringValue } var intValue: Int? { nil } init?(intValue: Int) { nil } } // 修改AnimalWrapper的解码逻辑 enum AnimalWrapper: Codable { case dog(Dog) case cat(Cat) init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: DynamicCodingKey.self) // 检查是否有type字段,是Dog;否则是Cat if container.contains(DynamicCodingKey(stringValue: "type")!) { self = .dog(try decoder.decode(Dog.self)) } else if container.contains(DynamicCodingKey(stringValue: "color")!) { self = .cat(try decoder.decode(Cat.self)) } else { throw DecodingError.dataCorrupted( DecodingError.Context( codingPath: [], debugDescription: "无法识别的动物类型" ) ) } } // 编码逻辑和方法一一致,这里省略 func encode(to encoder: Encoder) throws { // ... 同方法一的编码逻辑 } }
这种方法的缺点是扩展性差,如果以后新增其他遵循Animal的结构体,需要修改判断逻辑,不如方法一清晰。
总结
推荐使用方法一,通过添加类型标识字段+枚举包装的方式,逻辑清晰且扩展性好,能轻松应对后续新增的Animal类型。
内容的提问来源于stack exchange,提问作者user9041624
相关产品推荐
相关产品推荐

