【问题标题】:Swift: Codable - extract a single coding keySwift: Codable - 提取单个编码键
【发布时间】:2018-10-27 14:24:21
【问题描述】:

我有以下代码来提取包含在编码键中的 JSON:

let value = try! decoder.decode([String:Applmusic].self, from: $0["applmusic"])

这成功处理了以下 JSON:

{
  "applmusic":{
    "code":"AAPL",
    "quality":"good",
    "line":"She told me don't worry",
}

但是,无法从以下代码中提取编码密钥为applmusic 的 JSON:

{
  "applmusic":{
    "code":"AAPL",
    "quality":"good",
    "line":"She told me don't worry",
  },
  "spotify":{
    "differentcode":"SPOT",
    "music_quality":"good",
    "spotify_specific_code":"absent in apple"
  },
  "amazon":{
    "amzncode":"SPOT",
    "music_quality":"good",
    "stanley":"absent in apple"
  }
}

applmusicspotifyamazon 的数据模型不同。但是,我只需要提取applmusic 并省略其他编码键。

我的Swift 数据模型如下:

public struct Applmusic: Codable {
    public let code: String
    public let quality: String
    public let line: String
}

API 以完整的 JSON 响应,我不能要求它只给我所需的字段。

如何只解码 json 的特定部分?看来,Decodable 需要我先反序列化整个 json,所以我必须知道它的完整数据模型。

显然,其中一种解决方案是创建一个单独的 Response 模型以包含 applmusic 参数,但它看起来像一个 hack:

public struct Response: Codable {
    public struct Applmusic: Codable {
        public let code: String
        public let quality: String
        public let line: String
    }
    // The only parameter is `applmusic`, ignoring the other parts - works fine
    public let applmusic: Applmusic
}

您能否提出一种更好的方法来处理这种 JSON 结构?

更多见解

我在通用扩展中使用以下技术自动为我解码 API 响应。因此,我更愿意概括一种处理此类情况的方法,而无需创建Root 结构。如果我需要的密钥是 JSON 结构中的 3 层深度怎么办?

这是为我进行解码的扩展:

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")
  }
}

【问题讨论】:

  • 其中一种解决方案是只提取一个密钥的正确方法。
  • 你的Response 仍然是正确的方法尝试在单独的对象中获取applmusic 并尝试解码它可能会起作用
  • 您可以在init(decoder) 期间通过nestedContainer 完成此操作,而无需创建Response 包装器
  • @vadian 您能否详细说明“仅提取一个密钥的正确方法”?谢谢。
  • @Tj3n 你能在这里发布一个简短的例子吗?

标签: ios json swift codable decodable


【解决方案1】:

更新:我从这个答案中扩展了JSONDecoder,您可以在这里查看:https://github.com/aunnnn/NestedDecodable,它允许您使用关键路径解码任何深度的嵌套模型。

你可以这样使用它:

let post = try decoder.decode(Post.self, from: data, keyPath: "nested.post")

您可以制作一个 Decodable 包装器(例如,ModelResponse 此处),然后将所有逻辑用于提取嵌套模型,其中包含一个键:

struct DecodingHelper {

    /// Dynamic key
    private struct Key: CodingKey {
        let stringValue: String
        init?(stringValue: String) {
            self.stringValue = stringValue
            self.intValue = nil
        }

        let intValue: Int?
        init?(intValue: Int) {
            return nil
        }
    }

    /// Dummy model that handles model extracting logic from a key
    private struct ModelResponse<NestedModel: Decodable>: Decodable {
        let nested: NestedModel

        public init(from decoder: Decoder) throws {
            let key = Key(stringValue: decoder.userInfo[CodingUserInfoKey(rawValue: "my_model_key")!]! as! String)!
            let values = try decoder.container(keyedBy: Key.self)
            nested = try values.decode(NestedModel.self, forKey: key)
        }
    }

    static func decode<T: Decodable>(modelType: T.Type, fromKey key: String) throws -> T {
        // mock data, replace with network response
        let path = Bundle.main.path(forResource: "test", ofType: "json")!
        let data = try Data(contentsOf: URL(fileURLWithPath: path), options: .mappedIfSafe)

        let decoder = JSONDecoder()

        // ***Pass in our key through `userInfo`
        decoder.userInfo[CodingUserInfoKey(rawValue: "my_model_key")!] = key
        let model = try decoder.decode(ModelResponse<T>.self, from: data).nested
        return model
    }
}

您可以通过userInfoJSONDecoder ("my_model_key") 传递所需的密钥。然后将其转换为 ModelResponse 内部的动态 Key 以实际提取模型。

那么你可以这样使用它:

let appl = try DecodingHelper.decode(modelType: Applmusic.self, fromKey: "applmusic")
let amazon = try DecodingHelper.decode(modelType: Amazon.self, fromKey: "amazon")
let spotify = try DecodingHelper.decode(modelType: Spotify.self, fromKey: "spotify")
print(appl, amazon, spotify)

完整代码: https://gist.github.com/aunnnn/2d6bb20b9dfab41189a2411247d04904


奖励:深度嵌套的键

玩了更多之后,我发现你可以很容易地用这个修改过的ModelResponse解码任意深度的键:

private struct ModelResponse<NestedModel: Decodable>: Decodable {
    let nested: NestedModel

    public init(from decoder: Decoder) throws {
        // Split nested paths with '.'
        var keyPaths = (decoder.userInfo[CodingUserInfoKey(rawValue: "my_model_key")!]! as! String).split(separator: ".")

        // Get last key to extract in the end
        let lastKey = String(keyPaths.popLast()!)

        // Loop getting container until reach final one
        var targetContainer = try decoder.container(keyedBy: Key.self)
        for k in keyPaths {
            let key = Key(stringValue: String(k))!
            targetContainer = try targetContainer.nestedContainer(keyedBy: Key.self, forKey: key)
        }
        nested = try targetContainer.decode(NestedModel.self, forKey: Key(stringValue: lastKey)!)
    }

那么你可以这样使用它:

let deeplyNestedModel = try DecodingHelper.decode(modelType: Amazon.self, fromKey: "nest1.nest2.nest3")

来自这个 json:

{
    "apple": { ... },
    "amazon": {
        "amzncode": "SPOT",
        "music_quality": "good",
        "stanley": "absent in apple"
    },
    "nest1": {
        "nest2": {
            "amzncode": "Nest works",
            "music_quality": "Great",
            "stanley": "Oh yes",

            "nest3": {
                "amzncode": "Nest works, again!!!",
                "music_quality": "Great",
                "stanley": "Oh yes"
            }
        }
    }
}

完整代码:https://gist.github.com/aunnnn/9a6b4608ae49fe1594dbcabd9e607834

【讨论】:

  • 太棒了,只需要对 struct init 进行一些更改,就可以传入自定义 decoder 并像原始解码器一样接受数据,那就完美了
  • 谢谢。似乎是一个更通用的方法,我肯定会玩它......
【解决方案2】:

您实际上并不需要 Response 内的嵌套结构 Applmusic。这将完成这项工作:

import Foundation

let json = """
{
    "applmusic":{
        "code":"AAPL",
        "quality":"good",
        "line":"She told me don't worry"
    },
    "I don't want this":"potatoe",
}
"""

public struct Applmusic: Codable {
    public let code: String
    public let quality: String
    public let line: String
}

public struct Response: Codable {
    public let applmusic: Applmusic
}

if let data = json.data(using: .utf8) {
    let value = try! JSONDecoder().decode(Response.self, from: data).applmusic
    print(value) // Applmusic(code: "AAPL", quality: "good", line: "She told me don\'t worry")
}

编辑:处理您的最新评论

如果 JSON 响应以嵌套 applmusic 标记的方式发生变化,您只需正确更改您的 Response 类型。示例:

新 JSON(注意 applmusic 现在嵌套在新的 responseData 标记中):

{
    "responseData":{
        "applmusic":{
            "code":"AAPL",
            "quality":"good",
            "line":"She told me don't worry"
        },
        "I don't want this":"potatoe",
    }   
}

唯一需要更改的是Response

public struct Response: Decodable {

    public let applmusic: Applmusic

    enum CodingKeys: String, CodingKey {
        case responseData
    }

    enum ApplmusicKey: String, CodingKey {
        case applmusic
    }

    public init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)

        let applmusicKey = try values.nestedContainer(keyedBy: ApplmusicKey.self, forKey: .responseData)
        applmusic = try applmusicKey.decode(Applmusic.self, forKey: .applmusic)
    }
}

之前的更改不会破坏任何现有代码,我们只是微调 Response 如何解析 JSON 数据以正确获取 Applmusic 对象的私有实现。 JSONDecoder().decode(Response.self, from: data).applmusic 等所有调用都将保持不变。

提示

最后,如果你想完全隐藏Response 包装器逻辑,你可能有一个公共/公开的方法来完成所有的工作;如:

// (fine-tune this method to your needs)
func decodeAppleMusic(data: Data) throws -> Applmusic {
    return try JSONDecoder().decode(Response.self, from: data).applmusic
}

隐藏Response 甚至存在的事实(使其私有/不可访问),将允许您通过您的应用程序获得所有代码 必须调用decodeAppleMusic(data:)。例如:

if let data = json.data(using: .utf8) {
    let value = try! decodeAppleMusic(data: data)
    print(value) // Applmusic(code: "AAPL", quality: "good", line: "She told me don\'t worry")
}

推荐阅读:

编码和解码自定义类型

https://developer.apple.com/documentation/foundation/archives_and_serialization/encoding_and_decoding_custom_types

【讨论】:

    【解决方案3】:

    有趣的问题。我知道那是 2 周前,但我想知道 如何使用我创建的库KeyedCodable 来解决它。这是我对通用的提议:

    struct Response<Type>: Codable, Keyedable where Type: Codable {
    
        var responseObject: Type!
    
        mutating func map(map: KeyMap) throws {
            try responseObject <-> map[map.userInfo.keyPath]
        }
    
        init(from decoder: Decoder) throws {
            try KeyedDecoder(with: decoder).decode(to: &self)
        }
    }
    

    辅助扩展:

    private let infoKey = CodingUserInfoKey(rawValue: "keyPath")!
    extension Dictionary where Key == CodingUserInfoKey, Value == Any {
    
       var keyPath: String {
            set { self[infoKey] = newValue }
    
            get {
                guard let key = self[infoKey] as? String else { return "" }
                return key
            }
        }
    

    使用:

    let decoder = JSONDecoder()
    decoder.userInfo.keyPath = "applmusic"
    let response = try? decoder.decode(Response<Applmusic>.self, from: jsonData)
    

    请注意,keyPath 可能嵌套更深,我的意思是它可能是例如。 “responseData.services.applemusic”。

    此外,Response 是一个 Codable,因此您无需任何额外工作即可对其进行编码。

    【讨论】:

      猜你喜欢
      • 2019-05-12
      • 2021-05-24
      • 1970-01-01
      • 2020-05-30
      • 2019-05-04
      • 2017-12-25
      • 1970-01-01
      • 2018-11-15
      相关资源
      最近更新 更多