How to decode a nested JSON struct with Swift Decodable protocol?

Here is my JSON

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

Here is the structure I want it saved to (incomplete)

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

I have looked at Apple's Documentation on decoding nested structs, but I still do not understand how to do the different levels of the JSON properly. Any help will be much appreciated.

65367 次浏览

与其使用一个大的 CodingKeys枚举和解码 JSON 所需的键 所有,我建议使用嵌套枚举来保留层次结构,将嵌套 JSON 对象的 each的键分开:

// 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 数组进行解码,并被解码为 sequentially(即每次调用解码或嵌套容器方法时,它都会前进到数组中的下一个元素)。请参阅答案的第二部分,了解如何迭代一个问题。

在使用 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 非常匹配的中间模型(在 草草了事这样的工具的帮助下) ,让 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个值的话。

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


实现属性和所需的编码键

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


}

设置 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. Set the decoding strategy for userName property

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)


/* ... */
}


}

设置 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)


/* ... */
}


}

设置 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
}


}

Complete implementation

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
}


}

Usage

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
*/

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

许多好的答案已经张贴,但有一个更简单的方法尚未描述,国际海事组织。

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

你只需要设置

decoder.keyDecodingStrategy = .convertFromSnakeCase

在此 something line 之后,Swift 将自动匹配 Swift 模型中从 JSON 到 camelCase的所有 snake_case字段。

例如。

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)
}
  1. Copy the json file to 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)