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

132

这是我的 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?
    }
}

我已经查看过苹果的文档关于解码嵌套结构体的内容,但我仍然不明白如何正确处理JSON的不同层级。非常感谢帮助。


你只需要将所有内容嵌套成类,以匹配结构。就这么简单。 - Fattie
在Swift中没有一种“内联嵌套”的方法。你只需为每个嵌套创建一个新的结构体,就像@MojtabaHosseini的回答中所示。就是这样。 - undefined
8个回答

148

另一种方法是创建一个中间模型,它与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
    }
}

这也使您能够轻松迭代 reviews_count,如果将来它包含多个值。


1
好的,这种方法看起来非常简洁。对于我的情况,我想我会使用它。 - Just a coder
是的,我肯定想得太多了 - @JTAppleCalendarforiOSSwift 你应该接受它,因为这是一个更好的解决方案。 - Hamish
1
@nayem,问题在于ServerResponse的数据比RawServerResponse少。您可以捕获RawServerResponse实例,使用ServerResponse的属性更新它,然后从中生成JSON。如果您遇到具体问题,请发布一个新问题以获取更好的帮助。 - Code Different
完美解决方案!! - Vinayak Bhor
谢谢您提到 quicktype.io!这是一个很棒的工具。 - Mishka
显示剩余3条评论

146

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

24
非常专注的答案。 - Hexfire
3
你使用了enum和键来替代struct,这样做更加优雅。 - Jack
1
非常感谢您花时间将这个问题文档化得如此出色。在查找了那么多有关可解码和解析JSON的文献后,您的答案真正解决了我许多疑惑。 - Marcy
最佳答案!!!很好 - undefined

37

不要使用一个包含所有需要解码 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数组,并按顺序解码它(即每次调用decode或嵌套容器方法时,它都会在数组中前进到下一个元素)。有关如何迭代其中一个的第二部分,请参见答案。

在使用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" 键的值。你可以使用未有键容器的 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)
            )
        }
    }
}

有一件事需要澄清:你所说的“我建议将每个嵌套的JSON对象的键拆分成多个嵌套的枚举,从而更容易跟踪JSON中每个级别的键”的意思是什么? - Just a coder
@JTAppleCalendarforiOSSwift 我的意思是,与其拥有一个包含所有需要解码JSON对象的键的大型CodingKeys枚举,您应该将它们分成多个枚举,每个枚举对应一个JSON对象 - 例如,在上面的代码中,我们有CodingKeys.User,其中包含解码用户JSON对象({"user_name": "Tester","real_info": {"full_name": "Jon Doe"}})的键,因此只需使用“user_name”和“real_info”的键。 - Hamish
谢谢。非常清晰的回答。我还在仔细阅读以完全理解它。但它有效。 - Just a coder
我有一个关于reviews_count的问题,它是一个字典数组。目前,代码按预期工作。我的reviewsCount在数组中只有一个值。但是如果我实际上想要一个review_count的数组,那么我需要将var reviewCount: Int声明为数组,对吗?-> var reviewCount: [Int]。然后我还需要编辑ReviewsCount枚举,对吗? - Just a coder
1
@JTAppleCalendarforiOSSwift,这实际上会稍微复杂一些,因为您所描述的不仅仅是一个 Int 数组,而是一个包含每个键的 Int 值的 JSON 对象数组 - 所以您需要遍历未标记的容器并获取所有嵌套的带键容器,对于每个键解码一个 Int(然后将其附加到您的数组中),例如:https://gist.github.com/hamishknight/9b5c202fe6d8289ee2cb9403876a1b41 - Hamish

13
  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 let: guard let jsonStrData: Data? = jsonStr.data(using: .utf8)! else { print("error") }然后像上面 let yourObject 那行一样将 jsonStrData 转换为你自己的结构体。 - Ask P
这是一个惊人的工具! - PostCodeism

9

已经有许多好的回答了,但在我看来,还有一种更简单的方法未被描述。

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

7
这并没有回答原问题,即如何处理不同层次的嵌套。 - Theo
这是解码JSON最简单、最优雅的方式,当结构和类型不变时。如果它们发生了变化,你就必须手动解码字段。 - frin

2

你不需要像那些庞大的答案一样复杂!

你不需要解释自己。几乎所有东西都可以由Swift自动完成。

✅ 使用一个简单的扩展来处理你的地图:

extension Body {
    var username: String { user.userName }
    var fullName: String { user.realInfo.fullName }
    var reviewCount: Int { reviewsCount.first?.count ?? 0 }
}

你只需要像API一样“嵌套地”定义嵌套对象:

struct Body: Codable {
    let id: Int
    private let user: User
    private let reviewsCount: [ReviewCount]

    struct User: Codable {
        let userName: String
        let realInfo: RealInfo

        struct RealInfo: Codable {
            let fullName: String
        }
    }

    struct ReviewCount: Codable {
        let count: Int
    }
}

并使用具有正确keyDecodingStrategy的解码器进行解码:

let jsonDecoder: JSONDecoder = {
    var decoder = JSONDecoder()
    decoder.keyDecodingStrategy = .convertFromSnakeCase //  this will take care of the key namings
    return decoder
}()

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

0

一个解释性的答案:

目前的问题是:

“如何使用Swift解码嵌套的JSON结构…”

截至目前,Swift 没有这个功能:

struct Person: Codable {
    let height: String
    let hair: String
    let name: {
                let first: String
                let last: String
              }
}

你只需要输入这个:
struct Person: Codable {
    let height: String
    let hair: String
    let name: Name
}
struct Name: Codable {
    let first: String
    let last: String
}

那就是全部的事情。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接