ChatGPT解决这个技术问题 Extra ChatGPT

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

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


C
Code Different

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

// snake_case to match the JSON and hence no need to write CodingKey enums
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
    }
}

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


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

为了解决您的问题,您可以将 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 的文档之后,您的回答确实解决了我的许多问题。
H
Hamish

我建议不要为解码 JSON 所需的 all 键进行一个大的 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:) 从解码器获取顶级 keyed 容器后(因为顶级有 JSON 对象),您可以重复使用以下方法:

nestedContainer(keyedBy:forKey:) 从给定键的对象中获取嵌套对象

nestedUnkeyedContainer(forKey:) 从给定键的对象中获取嵌套数组

nestedContainer(keyedBy:) 从数组中获取下一个嵌套对象

nestedUnkeyedContainer() 从数组中获取下一个嵌套数组

例如:

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" 键的值。您可以使用无键容器的 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
L
Luca Angeletti

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

当使用 snake_case_notation 编写 JSON 字段名称时,您仍然可以在 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)
}

这并没有解决原始问题如何处理不同级别的嵌套。
这是在结构和类型不变的情况下解码 JSON 的最简单和最优雅的方法。如果他们这样做,您将不得不手动解码这些字段。
s
simibac

将 json 文件复制到 https://app.quicktype.io 选择 Swift(如果使用 Swift 5,请检查 Swift 5 的兼容性开关)使用以下代码解码文件瞧!

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 letguard let jsonStrData: Data? = jsonStr.data(using: .utf8)! else { print("error") } 然后将 jsonStrData 转换为您的结构,如上面 let yourObject 行中所述
这是一个了不起的工具!
d
decybel

您也可以使用我准备的库 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)
  }
}

关注公众号,不定期副业成功案例分享
关注公众号

不定期副业成功案例分享

领先一步获取最新的外包任务吗?

立即订阅