这是我的 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。任何帮助都感激不尽。
另一种方法是创建一个与 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
。
为了解决您的问题,您可以将 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 所需的 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
是什么意思?
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
枚举,对吗?
Int
数组,而是一个 JSON 对象数组,每个对象都有一个给定键的 Int
值 - 所以你会需要做的是遍历无键容器并获取所有嵌套的键容器,为每个容器解码一个 Int
(然后将它们附加到您的数组),例如 gist.github.com/hamishknight/9b5c202fe6d8289ee2cb9403876a1b41
已经发布了许多好的答案,但是还有一种更简单的方法尚未在 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 文件复制到 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)
jsonStr
,您可以使用它来代替上面的两个 guard let
:guard let jsonStrData: Data? = jsonStr.data(using: .utf8)! else { print("error") }
然后将 jsonStrData
转换为您的结构,如上面 let yourObject
行中所述
您也可以使用我准备的库 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)
}
}
ServerResponse
结构实现Encodable
。甚至可能吗?ServerResponse
的数据少于RawServerResponse
。您可以捕获RawServerResponse
实例,使用ServerResponse
中的属性对其进行更新,然后从中生成 JSON。您可以通过针对您面临的特定问题发布新问题来获得更好的帮助。