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


90

这是我的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的有关解码嵌套结构的文档,但是我仍然不明白如何正确执行JSON的不同级别。任何帮助都感激不尽。

Answers:


109

另一种方法是创建一个与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将来它包含多个值,这还使您可以轻松地进行迭代。


好。这种方法看起来很干净。就我而言,我想我会使用它
FlowUI。SimpleUITesting.com

是的,我绝对想不通了– @JTAppleCalendarforiOSSwift您应该接受它,因为它是更好的解决方案。
Hamish

@Hamish好的。我切换了,但是您的答案非常详细。我从中学到了很多。
FlowUI。SimpleUITesting.com

我很好奇知道如何使用相同的方法来实现EncodableServerResponse结构。可能吗
nayem

1
@nayem问题是ServerResponse数据少于RawServerResponse。您可以捕获RawServerResponse实例,使用中的属性对其进行更新ServerResponse,然后从中生成JSON。通过发布一个新问题以及您面临的特定问题,可以获得更好的帮助。
代码不同

94

为了解决您的问题,您可以将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
*/

13
非常专门的答案。
Hexfire

3
而不是struct您使用enum键。elegant
杰克(Jack)

1
非常感谢您抽出宝贵的时间来记录下来。在精读有关Decodable的大量文档并解析JSON之后,您的回答确实消除了我的许多问题。
Marcy

30

我建议不要对解码JSON所需的所有键进行一个大的CodingKeys枚举,而建议使用嵌套枚举保留层次结构,为每个嵌套的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中每个级别的键变得更加容易。

现在,请记住:

  • 键控容器用于解码JSON对象,并且被解码以CodingKey符合类型(如那些我们已经定义如上)。

  • 一个 密钥的容器用于解码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],其中每个元素代表"count"嵌套JSON中键的值:

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

您将需要遍历嵌套的无键容器,在每次迭代时获取嵌套的键控容器,并解码"count"键的值。您可以使用无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
FlowUI。SimpleUITesting.com

@JTAppleCalendarforiOSSwift我的意思是说,与其为解码JSON对象所需的所有CodingKeys提供一个大枚举,不如将每个JSON对象的它们分为多个枚举–例如,在上面的代码中,这些键具有解码用户JSON对象(),因此只需解码&即可。CodingKeys.User{ "user_name": "Tester", "real_info": { "full_name": "Jon Doe" } }"user_name""real_info"
Hamish

谢谢。反应非常明确。我仍在浏览以充分理解它。但这有效。
FlowUI。SimpleUITesting.com

我有一个关于的问题,reviews_count它是一个字典数组。当前,该代码按预期工作。我的reviewsCount在数组中只有一个值。但是,如果我实际上想要一个review_count数组,那我只需要简单地声明var reviewCount: Int为数组就可以了?-> var reviewCount: [Int]。然后我还需要编辑ReviewsCount枚举对吗?
FlowUI。SimpleUITesting.com

1
@JTAppleCalendarforiOSSwift这实际上会稍微复杂一点,因为您所描述的不仅仅是一个数组Int,而是一个JSON对象数组,每个JSON对象都具有Int给定键的值,因此您需要遍历无密钥的容器并获取所有嵌套的密钥容器,Int为每个密钥容器解码(然后将它们附加到数组中),例如gist.github.com/hamishknight/9b5c202fe6d8289ee2cb9403876a1b41
Hamish

4

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

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

您只需要设置

decoder.keyDecodingStrategy = .convertFromSnakeCase

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

例如

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

2
这没有解决原始问题,即如何处理不同级别的嵌套。
西奥,

2
  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)

1
为我工作,谢谢。那是黄金。对于观众,如果一个JSON字符串变量译码jsonStr,你可以用这个来代替两个guard letS的上方:guard let jsonStrData: Data? = jsonStr.data(using: .utf8)! else { print("error") }然后转换jsonStrData上面的描述,以你的结构let yourObject线
向P

这是一个了不起的工具!
PostCodeism

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)
  }
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.