首页 文章

如何使用Swift Decodable协议解码嵌套的JSON结构?

提问于
浏览
56

这是我的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 . 任何帮助都感激不尽 .

5 回答

  • 53

    另一种方法是创建一个与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个值 .

  • 54

    为了解决您的问题,您可以将 RawServerResponse 实现拆分为多个逻辑部分 .


    #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
    */
    
  • 19

    我不建议使用一个大的 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:)从解码器获取顶级键控容器后(因为您在顶层有一个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)
                )
            }
        }
    }
    
  • 12

    这些家伙已经回答了我的问题,但我只是觉得我会在这里发布这个链接,这使得这更容易 - > https://app.quicktype.io/#l=swift

    只需在左侧窗格中发布您的JSON响应,然后在右侧观看您的模型生成 . 当你刚开始时,这可以提供帮助 .

  • 0

    你也可以使用我编写的库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)
      }
    }
    

相关问题