【问题标题】:How to decode a nested JSON struct with Swift Decodable protocol?如何使用 Swift Decodable 协议解码嵌套的 JSON 结构?
【发布时间】:2017-11-16 20:58:09
【问题描述】:

这是我的 JSON

{
    "id": 1,
    "user": {
        "user_name": "Tester",
        "real_info": {
            "full_name":"Jon Doe"
        }
    },
    "reviews_count": [
        {
            "count": 4
        }
    ]
}

这是我想要保存到的结构(不完整)

struct ServerResponse: Decodable {
    var id: String
    var username: String
    var fullName: String
    var reviewCount: Int

    enum CodingKeys: String, CodingKey {
       case id, 
       // How do i get nested values?
    }
}

我在解码嵌套结构时查看了Apple's Documentation,但我仍然不明白如何正确执行不同级别的 JSON。任何帮助将不胜感激。

【问题讨论】:

    标签: json swift swift4 codable


    【解决方案1】:
    1. 将json文件复制到https://app.quicktype.io
    2. 选择 Swift(如果您使用 Swift 5,请检查 Swift 5 的兼容性开关)
    3. 使用以下代码解码文件
    4. 瞧!
    let file = "data.json"
    
    guard let url = Bundle.main.url(forResource: "data", withExtension: "json") else{
        fatalError("Failed to locate \(file) in bundle.")
    }
    
    guard let data = try? Data(contentsOf: url) else{
        fatalError("Failed to locate \(file) in bundle.")
    }
    
    let yourObject = try? JSONDecoder().decode(YourModel.self, from: data)
    

    【讨论】:

    • 为我工作,谢谢。那个网站是黄金。对于查看者,如果解码一个 json 字符串变量 jsonStr,您可以使用它而不是上面的两个 guard lets:guard let jsonStrData: Data? = jsonStr.data(using: .utf8)! else { print("error") } 然后将 jsonStrData 转换为您的结构,如上所述 let yourObject
    • 这是一个了不起的工具!
    【解决方案2】:

    已经发布了许多好的答案,但还有一种更简单的方法尚未在 IMO 中描述。

    当 JSON 字段名称使用 snake_case_notation 编写时,您仍然可以在 Swift 文件中使用 camelCaseNotation

    你只需要设置

    decoder.keyDecodingStrategy = .convertFromSnakeCase
    

    在这 ☝️ 行之后,Swift 会自动将 JSON 中的所有 snake_case 字段匹配到 Swift 模型中的 camelCase 字段。

    例如

    user_name` -> userName
    reviews_count -> `reviewsCount
    ...
    

    这是完整的代码

    1。编写模型

    struct Response: Codable {
    
        let id: Int
        let user: User
        let reviewsCount: [ReviewCount]
    
        struct User: Codable {
            let userName: String
    
            struct RealInfo: Codable {
                let fullName: String
            }
        }
    
        struct ReviewCount: Codable {
            let count: Int
        }
    }
    

    2。设置解码器

    let decoder = JSONDecoder()
    decoder.keyDecodingStrategy = .convertFromSnakeCase
    

    3。解码

    do {
        let response = try? decoder.decode(Response.self, from: data)
        print(response)
    } catch {
        debugPrint(error)
    }
    

    【讨论】:

    • 这并没有解决原始问题如何处理不同级别的嵌套。
    【解决方案3】:

    为了解决您的问题,您可以将 RawServerResponse 实现拆分为多个逻辑部分(使用 Swift 5)。


    #1。实现属性和所需的编码键

    import Foundation
    
    struct RawServerResponse {
    
        enum RootKeys: String, CodingKey {
            case id, user, reviewCount = "reviews_count"
        }
    
        enum UserKeys: String, CodingKey {
            case userName = "user_name", realInfo = "real_info"
        }
    
        enum RealInfoKeys: String, CodingKey {
            case fullName = "full_name"
        }
    
        enum ReviewCountKeys: String, CodingKey {
            case count
        }
    
        let id: Int
        let userName: String
        let fullName: String
        let reviewCount: Int
    
    }
    

    #2。设置id属性的解码策略

    extension RawServerResponse: Decodable {
    
        init(from decoder: Decoder) throws {
            // id
            let container = try decoder.container(keyedBy: RootKeys.self)
            id = try container.decode(Int.self, forKey: .id)
    
            /* ... */                 
        }
    
    }
    

    #3。设置userName属性的解码策略

    extension RawServerResponse: Decodable {
    
        init(from decoder: Decoder) throws {
            /* ... */
    
            // userName
            let userContainer = try container.nestedContainer(keyedBy: UserKeys.self, forKey: .user)
            userName = try userContainer.decode(String.self, forKey: .userName)
    
            /* ... */
        }
    
    }
    

    #4。设置fullName属性的解码策略

    extension RawServerResponse: Decodable {
    
        init(from decoder: Decoder) throws {
            /* ... */
    
            // fullName
            let realInfoKeysContainer = try userContainer.nestedContainer(keyedBy: RealInfoKeys.self, forKey: .realInfo)
            fullName = try realInfoKeysContainer.decode(String.self, forKey: .fullName)
    
            /* ... */
        }
    
    }
    

    #5。设置reviewCount属性的解码策略

    extension RawServerResponse: Decodable {
    
        init(from decoder: Decoder) throws {
            /* ...*/        
    
            // reviewCount
            var reviewUnkeyedContainer = try container.nestedUnkeyedContainer(forKey: .reviewCount)
            var reviewCountArray = [Int]()
            while !reviewUnkeyedContainer.isAtEnd {
                let reviewCountContainer = try reviewUnkeyedContainer.nestedContainer(keyedBy: ReviewCountKeys.self)
                reviewCountArray.append(try reviewCountContainer.decode(Int.self, forKey: .count))
            }
            guard let reviewCount = reviewCountArray.first else {
                throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: container.codingPath + [RootKeys.reviewCount], debugDescription: "reviews_count cannot be empty"))
            }
            self.reviewCount = reviewCount
        }
    
    }
    

    完成实施

    import Foundation
    
    struct RawServerResponse {
    
        enum RootKeys: String, CodingKey {
            case id, user, reviewCount = "reviews_count"
        }
    
        enum UserKeys: String, CodingKey {
            case userName = "user_name", realInfo = "real_info"
        }
    
        enum RealInfoKeys: String, CodingKey {
            case fullName = "full_name"
        }
    
        enum ReviewCountKeys: String, CodingKey {
            case count
        }
    
        let id: Int
        let userName: String
        let fullName: String
        let reviewCount: Int
    
    }
    
    extension RawServerResponse: Decodable {
    
        init(from decoder: Decoder) throws {
            // id
            let container = try decoder.container(keyedBy: RootKeys.self)
            id = try container.decode(Int.self, forKey: .id)
    
            // userName
            let userContainer = try container.nestedContainer(keyedBy: UserKeys.self, forKey: .user)
            userName = try userContainer.decode(String.self, forKey: .userName)
    
            // fullName
            let realInfoKeysContainer = try userContainer.nestedContainer(keyedBy: RealInfoKeys.self, forKey: .realInfo)
            fullName = try realInfoKeysContainer.decode(String.self, forKey: .fullName)
    
            // reviewCount
            var reviewUnkeyedContainer = try container.nestedUnkeyedContainer(forKey: .reviewCount)
            var reviewCountArray = [Int]()
            while !reviewUnkeyedContainer.isAtEnd {
                let reviewCountContainer = try reviewUnkeyedContainer.nestedContainer(keyedBy: ReviewCountKeys.self)
                reviewCountArray.append(try reviewCountContainer.decode(Int.self, forKey: .count))
            }
            guard let reviewCount = reviewCountArray.first else {
                throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: container.codingPath + [RootKeys.reviewCount], debugDescription: "reviews_count cannot be empty"))
            }
            self.reviewCount = reviewCount
        }
    
    }
    

    用法

    let jsonString = """
    {
        "id": 1,
        "user": {
            "user_name": "Tester",
            "real_info": {
                "full_name":"Jon Doe"
            }
        },
        "reviews_count": [
        {
        "count": 4
        }
        ]
    }
    """
    
    let jsonData = jsonString.data(using: .utf8)!
    let decoder = JSONDecoder()
    let serverResponse = try! decoder.decode(RawServerResponse.self, from: jsonData)
    dump(serverResponse)
    
    /*
    prints:
    ▿ RawServerResponse #1 in __lldb_expr_389
      - id: 1
      - user: "Tester"
      - fullName: "Jon Doe"
      - reviewCount: 4
    */
    

    【讨论】:

    • 非常专业的答案。
    • 您使用enum 代替了struct 和键。哪个更优雅?
    • 非常感谢您抽出宝贵的时间来记录这一点。在搜索了这么多有关可解码和解析 JSON 的文档之后,您的回答确实解决了我的许多问题。
    【解决方案4】:

    另一种方法是创建一个与 JSON 紧密匹配的中间模型(借助 quicktype.io 之类的工具),让 Swift 生成对其进行解码的方法,然后在最终结果中挑选出您想要的部分数据模型:

    // snake_case to match the JSON and hence no need to write CodingKey enums / struct
    fileprivate struct RawServerResponse: Decodable {
        struct User: Decodable {
            var user_name: String
            var real_info: UserRealInfo
        }
    
        struct UserRealInfo: Decodable {
            var full_name: String
        }
    
        struct Review: Decodable {
            var count: Int
        }
    
        var id: Int
        var user: User
        var reviews_count: [Review]
    }
    
    struct ServerResponse: Decodable {
        var id: String
        var username: String
        var fullName: String
        var reviewCount: Int
    
        init(from decoder: Decoder) throws {
            let rawResponse = try RawServerResponse(from: decoder)
    
            // Now you can pick items that are important to your data model,
            // conveniently decoded into a Swift structure
            id = String(rawResponse.id)
            username = rawResponse.user.user_name
            fullName = rawResponse.user.real_info.full_name
            reviewCount = rawResponse.reviews_count.first!.count
        }
    }
    

    这还允许您轻松地遍历 reviews_count,如果它将来包含超过 1 个值。

    【讨论】:

    • 好的。这种方法看起来很干净。对于我的情况,我想我会使用它
    • 是的,我确实想多了——@JTAppleCalendarforiOSSwift 你应该接受它,因为它是一个更好的解决方案。
    • @Hamish 好的。我换了,但你的回答非常详细。我从中学到了很多。
    • 我很想知道如何按照相同的方法为ServerResponse 结构实现Encodable。有可能吗?
    • @nayem 问题是ServerResponse 的数据少于RawServerResponse。您可以捕获 RawServerResponse 实例,使用来自 ServerResponse 的属性对其进行更新,然后从中生成 JSON。您可以通过针对您面临的具体问题发布新问题来获得更好的帮助。
    【解决方案5】:

    您也可以使用我准备的库KeyedCodable。它将需要更少的代码。让我知道你的想法。

    struct ServerResponse: Decodable, Keyedable {
      var id: String!
      var username: String!
      var fullName: String!
      var reviewCount: Int!
    
      private struct ReviewsCount: Codable {
        var count: Int
      }
    
      mutating func map(map: KeyMap) throws {
        var id: Int!
        try id <<- map["id"]
        self.id = String(id)
    
        try username <<- map["user.user_name"]
        try fullName <<- map["user.real_info.full_name"]
    
        var reviewCount: [ReviewsCount]!
        try reviewCount <<- map["reviews_count"]
        self.reviewCount = reviewCount[0].count
      }
    
      init(from decoder: Decoder) throws {
        try KeyedDecoder(with: decoder).decode(to: &self)
      }
    }
    

    【讨论】:

      【解决方案6】:

      与其拥有一个大的 CodingKeys 枚举和 所有 用于解码 JSON 所需的密钥,我建议将密钥拆分为您的 每个嵌套 JSON 对象,使用嵌套枚举来保留层次结构:

      // top-level JSON object keys
      private enum CodingKeys : String, CodingKey {
      
          // using camelCase case names, with snake_case raw values where necessary.
          // the raw values are what's used as the actual keys for the JSON object,
          // and default to the case name unless otherwise specified.
          case id, user, reviewsCount = "reviews_count"
      
          // "user" JSON object keys
          enum User : String, CodingKey {
              case username = "user_name", realInfo = "real_info"
      
              // "real_info" JSON object keys
              enum RealInfo : String, CodingKey {
                  case fullName = "full_name"
              }
          }
      
          // nested JSON objects in "reviews" keys
          enum ReviewsCount : String, CodingKey {
              case count
          }
      }
      

      这样可以更轻松地跟踪 JSON 中每个级别的键。

      现在,请记住:

      • keyed container 用于解码 JSON 对象,并使用符合 CodingKey 的类型(例如我们上面定义的类型)进行解码。

      • unkeyed container 用于解码 JSON 数组,并按顺序进行解码(即每次调用解码或嵌套容器方法时,它都会前进到下一个元素在数组中)。请参阅答案的第二部分,了解如何迭代一个。

      在使用container(keyedBy:) 从解码器获取您的顶级keyed 容器后(因为您在顶级有一个JSON 对象),您可以重复使用这些方法:

      例如:

      struct ServerResponse : Decodable {
      
          var id: Int, username: String, fullName: String, reviewCount: Int
      
          private enum CodingKeys : String, CodingKey { /* see above definition in answer */ }
      
          init(from decoder: Decoder) throws {
      
              // top-level container
              let container = try decoder.container(keyedBy: CodingKeys.self)
              self.id = try container.decode(Int.self, forKey: .id)
      
              // container for { "user_name": "Tester", "real_info": { "full_name": "Jon Doe" } }
              let userContainer =
                  try container.nestedContainer(keyedBy: CodingKeys.User.self, forKey: .user)
      
              self.username = try userContainer.decode(String.self, forKey: .username)
      
              // container for { "full_name": "Jon Doe" }
              let realInfoContainer =
                  try userContainer.nestedContainer(keyedBy: CodingKeys.User.RealInfo.self,
                                                    forKey: .realInfo)
      
              self.fullName = try realInfoContainer.decode(String.self, forKey: .fullName)
      
              // container for [{ "count": 4 }] – must be a var, as calling a nested container
              // method on it advances it to the next element.
              var reviewCountContainer =
                  try container.nestedUnkeyedContainer(forKey: .reviewsCount)
      
              // container for { "count" : 4 }
              // (note that we're only considering the first element of the array)
              let firstReviewCountContainer =
                  try reviewCountContainer.nestedContainer(keyedBy: CodingKeys.ReviewsCount.self)
      
              self.reviewCount = try firstReviewCountContainer.decode(Int.self, forKey: .count)
          }
      }
      

      解码示例:

      let jsonData = """
      {
        "id": 1,
        "user": {
          "user_name": "Tester",
          "real_info": {
          "full_name":"Jon Doe"
        }
        },
        "reviews_count": [
          {
            "count": 4
          }
        ]
      }
      """.data(using: .utf8)!
      
      do {
          let response = try JSONDecoder().decode(ServerResponse.self, from: jsonData)
          print(response)
      } catch {
          print(error)
      }
      
      // ServerResponse(id: 1, username: "Tester", fullName: "Jon Doe", reviewCount: 4)
      

      遍历无键容器

      考虑您希望reviewCount 成为[Int] 的情况,其中每个元素表示嵌套JSON 中"count" 键的值:

        "reviews_count": [
          {
            "count": 4
          },
          {
            "count": 5
          }
        ]
      

      您需要遍历嵌套的无键容器,在每次迭代时获取嵌套的键容器,并解码 "count" 键的值。您可以使用 unkeyed 容器的 count 属性来预分配结果数组,然后使用 isAtEnd 属性对其进行迭代。

      例如:

      struct ServerResponse : Decodable {
      
          var id: Int
          var username: String
          var fullName: String
          var reviewCounts = [Int]()
      
          // ...
      
          init(from decoder: Decoder) throws {
      
              // ...
      
              // container for [{ "count": 4 }, { "count": 5 }]
              var reviewCountContainer =
                  try container.nestedUnkeyedContainer(forKey: .reviewsCount)
      
              // pre-allocate the reviewCounts array if we can
              if let count = reviewCountContainer.count {
                  self.reviewCounts.reserveCapacity(count)
              }
      
              // iterate through each of the nested keyed containers, getting the
              // value for the "count" key, and appending to the array.
              while !reviewCountContainer.isAtEnd {
      
                  // container for a single nested object in the array, e.g { "count": 4 }
                  let nestedReviewCountContainer = try reviewCountContainer.nestedContainer(
                                                       keyedBy: CodingKeys.ReviewsCount.self)
      
                  self.reviewCounts.append(
                      try nestedReviewCountContainer.decode(Int.self, forKey: .count)
                  )
              }
          }
      }
      

      【讨论】:

      • 澄清一点:I would advise splitting the keys for each of your nested JSON objects up into multiple nested enumerations, thereby making it easier to keep track of the keys at each level in your JSON 是什么意思?
      • @JTAppleCalendarforiOSSwift 我的意思是,与其拥有一个大的 CodingKeys 枚举和 all 键来解码 JSON 对象,不如将它们分成多个每个 JSON 对象的枚举——例如,在上面的代码中,我们有 CodingKeys.User 和用于解码用户 JSON 对象 ({ "user_name": "Tester", "real_info": { "full_name": "Jon Doe" } }) 的键,所以只有 "user_name""real_info" 的键。
      • 谢谢。非常明确的回应。我仍在浏览它以完全理解它。但它有效。
      • 我有一个关于 reviews_count 的问题,它是一个字典数组。目前,代码按预期工作。我的 reviewsCount 在数组中只有一个值。但是,如果我真的想要一个 review_count 数组,那么我需要简单地将 var reviewCount: Int 声明为一个数组,对吗? ->var reviewCount: [Int]。然后我还需要编辑 ReviewsCount 枚举,对吗?
      • @JTAppleCalendarforiOSSwift 这实际上会稍微复杂一些,因为您所描述的不仅仅是一个 Int 数组,而是一个 JSON 对象数组,每个对象都有一个 Int 值给定密钥——所以你需要做的是遍历未加密钥的容器并获取所有嵌套的密钥容器,为每个容器解码一个Int(然后将它们附加到你的数组中),例如gist.github.com/hamishknight/9b5c202fe6d8289ee2cb9403876a1b41
      猜你喜欢
      • 2021-08-07
      • 1970-01-01
      • 2017-11-19
      • 2021-01-14
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-11-29
      • 1970-01-01
      相关资源
      最近更新 更多