Swift Codable:如何仅提取指定编码键对应的JSON内容?
我现在有一段Swift代码用来提取指定编码键对应的JSON内容:
let value = try! decoder.decode([String:Applmusic].self, from: $0["applmusic"])
这段代码能处理仅含applmusic键的JSON,但没法从包含spotify、amazon等其他键的完整JSON里提取applmusic对应的数据。
我的Applmusic数据模型是这样的:
public struct Applmusic: Codable { public let code: String public let quality: String public let line: String }
API返回的是包含多个键的完整JSON,没办法要求只返回我需要的字段。目前我能通过创建一个仅包含applmusic的Response根模型来解决,但感觉这种方式不够优雅。
另外,我在通用的Endpoint扩展里自动处理API响应的解码,希望不需要创建根结构,同时还能适配编码键嵌套多层的场景。相关的扩展和API定义如下:
Endpoint扩展:
extension Endpoint where Response: Swift.Decodable { convenience init(method: Method = .get, path: Path, codingKey: String? = nil, parameters: Parameters? = nil) { self.init(method: method, path: path, parameters: parameters, codingKey: codingKey) { if let key = codingKey { guard let value = try decoder.decode([String:Response].self, from: $0)[key] else { throw RestClientError.valueNotFound(codingKey: key) } return value } return try decoder.decode(Response.self, from: $0) } } }
API定义:
extension API { static func getMusic() -> Endpoint<[Applmusic]> { return Endpoint(method: .get, path: "/api/music", codingKey: "applmusic") } }
想请教有没有更优的方案来处理这类JSON结构?
嘿,这个问题我太懂了——为了一个字段专门写个根模型确实挺冗余的,而且还不支持嵌套键,给你分享两个更灵活的方案,完美适配你通用Endpoint的需求:
方案1:自定义解码容器扩展,支持任意层级键路径
我们可以给KeyedDecodingContainer加个扩展,让它能直接根据键路径(比如"data.music.applmusic"这种多层的)解码目标类型,完全不需要额外的根模型:
// 先定义一个通用的CodingKey,用来处理动态字符串键 private struct GenericCodingKey: CodingKey { let stringValue: String let intValue: Int? init?(stringValue: String) { self.stringValue = stringValue self.intValue = nil } init?(intValue: Int) { self.intValue = intValue self.stringValue = String(intValue) } } // 扩展KeyedDecodingContainer,添加键路径解码方法 extension KeyedDecodingContainer { func decode<T: Decodable>(_ type: T.Type, forKeyPath keyPath: String) throws -> T { // 把键路径拆分成单个键 let keys = keyPath.components(separatedBy: ".") var currentContainer = self // 逐层进入嵌套容器 for key in keys.dropLast() { guard let codingKey = GenericCodingKey(stringValue: key) else { throw DecodingError.dataCorrupted(.init( codingPath: currentContainer.codingPath, debugDescription: "无效的键:\(key)" )) } currentContainer = try currentContainer.nestedContainer(keyedBy: GenericCodingKey.self, forKey: codingKey) } // 解码最后一层的目标值 guard let lastKey = keys.last, let codingKey = GenericCodingKey(stringValue: lastKey) else { throw DecodingError.dataCorrupted(.init( codingPath: currentContainer.codingPath, debugDescription: "无效的键路径:\(keyPath)" )) } return try currentContainer.decode(T.self, forKey: codingKey) } }
然后修改你的Endpoint扩展,用这个新方法替换原来的[String:Response]解码逻辑:
extension Endpoint where Response: Swift.Decodable { convenience init( method: Method = .get, path: Path, codingKeyPath: String? = nil, // 把参数名改成更贴合的codingKeyPath parameters: Parameters? = nil ) { self.init(method: method, path: path, parameters: parameters, codingKey: codingKeyPath) { data in let decoder = JSONDecoder() // 这里可以根据你的需求配置decoder,比如日期解码策略、蛇形命名转驼峰等 if let keyPath = codingKeyPath { // 先把整个JSON解码成键控容器,再按路径提取 let rootContainer = try decoder.decode(KeyedDecodingContainer<GenericCodingKey>.self, from: data) return try rootContainer.decode(Response.self, forKeyPath: keyPath) } return try decoder.decode(Response.self, from: data) } } }
现在你的API定义不仅支持单层键,还能直接传多层嵌套的键路径:
extension API { static func getMusic() -> Endpoint<[Applmusic]> { // 单层键 return Endpoint(method: .get, path: "/api/music", codingKeyPath: "applmusic") // 如果是多层嵌套,比如applmusic在data下面: // return Endpoint(method: .get, path: "/api/music", codingKeyPath: "data.applmusic") } }
这个方案的优势是灵活性拉满,不管JSON嵌套多少层都能处理,而且直接操作解码容器,性能也很好。
方案2:泛型包装器,集中处理键提取
如果你觉得扩展解码容器太底层,也可以用一个泛型包装器来封装键提取逻辑,代码更简洁:
// 通用CodingKey还是需要的 private struct GenericCodingKey: CodingKey { let stringValue: String let intValue: Int? init?(stringValue: String) { self.stringValue = stringValue self.intValue = nil } init?(intValue: Int) { self.intValue = intValue self.stringValue = String(intValue) } } // 给CodingUserInfoKey加个键,用来传递我们要提取的编码键 extension CodingUserInfoKey { static let targetCodingKey = CodingUserInfoKey(rawValue: "targetCodingKey")! } // 泛型包装器,负责从JSON中提取指定键的内容 struct KeyedResponseWrapper<T: Decodable>: Decodable { let value: T init(from decoder: Decoder) throws { // 从解码器的userInfo里获取目标键 guard let targetKey = decoder.userInfo[.targetCodingKey] as? String else { throw DecodingError.dataCorrupted(.init( codingPath: [], debugDescription: "未在解码器userInfo中指定目标编码键" )) } let container = try decoder.container(keyedBy: GenericCodingKey.self) guard let codingKey = GenericCodingKey(stringValue: targetKey) else { throw DecodingError.dataCorrupted(.init( codingPath: [], debugDescription: "无效的目标编码键:\(targetKey)" )) } self.value = try container.decode(T.self, forKey: codingKey) } }
然后修改Endpoint的解码逻辑:
extension Endpoint where Response: Swift.Decodable { convenience init( method: Method = .get, path: Path, codingKey: String? = nil, parameters: Parameters? = nil ) { self.init(method: method, path: path, parameters: parameters, codingKey: codingKey) { data in let decoder = JSONDecoder() if let key = codingKey { // 把目标键存入解码器的userInfo decoder.userInfo[.targetCodingKey] = key // 解码包装器,取出里面的目标值 let wrapper = try decoder.decode(KeyedResponseWrapper<Response>.self, from: data) return wrapper.value } return try decoder.decode(Response.self, from: data) } } }
这个方案的逻辑更集中,如果你不需要处理多层嵌套,用起来非常顺手;如果要支持多层,只需要在KeyedResponseWrapper的解码逻辑里加上方案1的键路径拆分逻辑就行。
方案选择建议
- 如果你的API有很多多层嵌套的JSON结构,优先选方案1,灵活性和扩展性都更强。
- 如果大部分场景都是单层键,方案2的代码更简洁,理解成本更低。
两种方案都不需要创建多余的根模型,完美适配你通用Endpoint的自动解码需求~
内容的提问来源于stack exchange,提问作者Richard Topchii

