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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.28 04:23:03