Swift 4 可解码,直到解码时才知道密钥

Posted

技术标签:

【中文标题】Swift 4 可解码,直到解码时才知道密钥【英文标题】:Swift 4 Decodable with keys not known until decoding time 【发布时间】:2018-01-17 19:15:59 【问题描述】:

Swift 4 Decodable 协议如何处理包含一个直到运行时才知道名称的键的字典?例如:

  [
    
      "categoryName": "Trending",
      "Trending": [
        
          "category": "Trending",
          "trailerPrice": "",
          "isFavourit": null,
          "isWatchlist": null
        
      ]
    ,
    
      "categoryName": "Comedy",
      "Comedy": [
        
          "category": "Comedy",
          "trailerPrice": "",
          "isFavourit": null,
          "isWatchlist": null
        
      ]
    
  ]

这里有一个字典数组;第一个有键categoryNameTrending,而第二个有键categoryNameComedycategoryName 键的值告诉我第二个键的名称。我如何使用 Decodable 来表达?

【问题讨论】:

【参考方案1】:

这是我最终想出的这个 json:

let json = """

    "BTC_BCN":
        "last":"0.00000057",
        "percentChange":"0.03636363",
        "baseVolume":"47.08463318"
    ,
    "BTC_BELA":
        "last":"0.00001281",
        "percentChange":"0.07376362",
        "baseVolume":"5.46595029"
    

""".data(using: .utf8)!

我们做了这样的结构:

struct Pair 
    let name: String
    let details: Details

    struct Details: Codable 
        let last, percentChange, baseVolume: String
    

然后解码:

if let pairsDictionary = try? JSONDecoder().decode([String: Pair.Details].self, from: json) 
    
    var pairs: [Pair] = []
    for (name, details) in pairsDictionary 
        let pair = Pair(name: name, details: details)
        pairs.append(pair)
    
    
    print(pairs)

也可以不调用pair.details.baseVolume,而是调用pair.baseVolume:

struct Pair 
    ......
    var baseVolume: String  return details.baseVolume 
    ......

或者编写自定义初始化:

struct Pair 
    .....
    let baseVolume: String
    init(name: String, details: Details) 
         self.baseVolume = details.baseVolume
    ......

【讨论】:

【参考方案2】:

关键在于如何定义CodingKeys 属性。虽然它最常见的是enum,但它可以是任何符合CodingKey 协议的东西。要制作动态键,您可以调用静态函数:

struct Category: Decodable 
    struct Detail: Decodable 
        var category: String
        var trailerPrice: String
        var isFavorite: Bool?
        var isWatchlist: Bool?
    

    var name: String
    var detail: Detail

    private struct CodingKeys: CodingKey 
        var intValue: Int?
        var stringValue: String

        init?(intValue: Int)  self.intValue = intValue; self.stringValue = "\(intValue)" 
        init?(stringValue: String)  self.stringValue = stringValue 

        static let name = CodingKeys.make(key: "categoryName")
        static func make(key: String) -> CodingKeys 
            return CodingKeys(stringValue: key)!
        
    

    init(from coder: Decoder) throws 
        let container = try coder.container(keyedBy: CodingKeys.self)
        self.name = try container.decode(String.self, forKey: .name)
        self.detail = try container.decode([Detail].self, forKey: .make(key: name)).first!
    

用法:

let jsonData = """
  [
    
      "categoryName": "Trending",
      "Trending": [
        
          "category": "Trending",
          "trailerPrice": "",
          "isFavourite": null,
          "isWatchlist": null
        
      ]
    ,
    
      "categoryName": "Comedy",
      "Comedy": [
        
          "category": "Comedy",
          "trailerPrice": "",
          "isFavourite": null,
          "isWatchlist": null
        
      ]
    
  ]
""".data(using: .utf8)!

let categories = try! JSONDecoder().decode([Category].self, from: jsonData)

(我将 JSON 中的 isFavourit 更改为 isFavourite,因为我认为这是拼写错误。如果不是这种情况,修改代码很容易)

【讨论】:

当您回答时,我想出了一个非常相似的解决方案;我一会儿贴出来,你看看你的想法。 显然你的更好,但我很高兴我独立地想到了一些东西。花了我一整天! 这是否适用于动态键实际上永远不知道的情况? ***.com/questions/46726415/… 谁有任何关于动态类型的帖子的链接?键名是已知的,数据将始终存在,但它可以是字符串或 Int。 @Martheli 发布一个新问题,其中包含您的问题的详细信息,有人会查看它。您不会从评论中的问题中得到任何答案【参考方案3】:

您可以编写一个用作 CodingKeys 对象的自定义结构,并使用字符串对其进行初始化,以便提取您指定的键:

private struct CK : CodingKey 
    var stringValue: String
    init?(stringValue: String) 
        self.stringValue = stringValue
    
    var intValue: Int?
    init?(intValue: Int) 
        return nil
    

因此,一旦您知道所需的密钥是什么,您就可以说(在 init(from:) 覆盖中:

let key = // whatever the key name turns out to be
let con2 = try! decoder.container(keyedBy: CK.self)
self.unknown = try! con2.decode([Inner].self, forKey: CK(stringValue:key)!)

所以我最终做的是从解码器中制作 两个 容器——一个使用标准 CodingKeys 枚举来提取 "categoryName" 键的值,另一个使用 CK 结构来提取我们刚刚学习的键名的值:

init(from decoder: Decoder) throws 
    let con = try! decoder.container(keyedBy: CodingKeys.self)
    self.categoryName = try! con.decode(String.self, forKey:.categoryName)
    let key = self.categoryName
    let con2 = try! decoder.container(keyedBy: CK.self)
    self.unknown = try! con2.decode([Inner].self, forKey: CK(stringValue:key)!)

那么,这里是我的整个 Decodable 结构:

struct ResponseData : Codable 
    let categoryName : String
    let unknown : [Inner]
    struct Inner : Codable 
        let category : String
        let trailerPrice : String
        let isFavourit : String?
        let isWatchList : String?
    
    private enum CodingKeys : String, CodingKey 
        case categoryName
    
    private struct CK : CodingKey 
        var stringValue: String
        init?(stringValue: String) 
            self.stringValue = stringValue
        
        var intValue: Int?
        init?(intValue: Int) 
            return nil
        
    
    init(from decoder: Decoder) throws 
        let con = try! decoder.container(keyedBy: CodingKeys.self)
        self.categoryName = try! con.decode(String.self, forKey:.categoryName)
        let key = self.categoryName
        let con2 = try! decoder.container(keyedBy: CK.self)
        self.unknown = try! con2.decode([Inner].self, forKey: CK(stringValue:key)!)
    

这里是测试台:

    let json = """
      [
        
          "categoryName": "Trending",
          "Trending": [
            
              "category": "Trending",
              "trailerPrice": "",
              "isFavourit": null,
              "isWatchlist": null
            
          ]
        ,
        
          "categoryName": "Comedy",
          "Comedy": [
            
              "category": "Comedy",
              "trailerPrice": "",
              "isFavourit": null,
              "isWatchlist": null
            
          ]
        
      ]
    """
    let myjson = try! JSONDecoder().decode(
        [ResponseData].self, 
        from: json.data(using: .utf8)!)
    print(myjson)

这里是 print 语句的输出,证明我们已经正确地填充了我们的结构:

[JustPlaying.ResponseData(
    categoryName: "Trending", 
    unknown: [JustPlaying.ResponseData.Inner(
        category: "Trending", 
        trailerPrice: "", 
        isFavourit: nil, 
        isWatchList: nil)]), 
 JustPlaying.ResponseData(
    categoryName: "Comedy", 
    unknown: [JustPlaying.ResponseData.Inner(
        category: "Comedy", 
        trailerPrice: "", 
        isFavourit: nil, 
        isWatchList: nil)])
]

当然,在现实生活中我们会有一些错误处理,毫无疑问!


编辑 后来我意识到(部分感谢 CodeDifferent 的回答)我不需要两个容器;我可以消除 CodingKeys 枚举,而我的 CK 结构可以完成所有工作!它是一个通用的密钥生成器:

init(from decoder: Decoder) throws 
    let con = try! decoder.container(keyedBy: CK.self)
    self.categoryName = try! con.decode(String.self, forKey:CK(stringValue:"categoryName")!)
    let key = self.categoryName
    self.unknown = try! con.decode([Inner].self, forKey: CK(stringValue:key)!)

【讨论】:

我们基本上想出了相同的解决方案! 是的,但是你赢了。我从来没有想过单独使用结构。然而现在已经很明显了。 :)

以上是关于Swift 4 可解码,直到解码时才知道密钥的主要内容,如果未能解决你的问题,请参考以下文章

带有可配置键的 Swift 4 JSON 解码

Swift 4 可解码的 json 数组

仅在运行时知道类型时的 Swift 4 JSON 解码

Swift 4 可编码;如何使用单个根级密钥解码对象

当您不知道 Swift 中的项目类型时,如何解码嵌套的 JSON 数据? [复制]

JSON解码器Swift 4.0