Как декодировать вложенную структуру JSON с протоколом Swift Decodable?

Вот мой 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. Любая помощь будет высоко ценится.

Другой подход заключается в создании промежуточной модели, которая точно соответствует JSON (с помощью такого инструмента, как quicktype.io ), пусть Swift генерирует методы для его декодирования, а затем отбирает fragmentы, которые вы хотите в своей последней модели данных:

 // 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 , если в будущем оно будет содержать более 1 значения.

Чтобы решить вашу проблему, вы можете разделить реализацию RawServerResponse на несколько логических частей.


# 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. Задайте страtagsю декодирования для свойства 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. Задайте страtagsю декодирования для свойства 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. Задайте страtagsю декодирования для свойства 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. Задайте страtagsю декодирования для свойства 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 */ 

Вместо того, чтобы иметь одно большое перечисление CodingKeys со всеми ключами, которые вам нужны для декодирования JSON, я бы посоветовал разделить ключи для каждого из ваших вложенных объектов JSON, используя вложенные enums для сохранения иерархии:

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

Теперь, имея в виду, что:

  • Контейнер с CodingKey используется для декодирования объекта JSON и декодируется с соответствующим типом CodingKey (например, теми, которые мы определили выше).

  • Коннектор без ключа используется для декодирования массива JSON и последовательно декодируется (т.е. каждый раз, когда вы вызываете на нем метод декодирования или вложенного контейнера, он переходит к следующему элементу массива). См. Вторую часть ответа о том, как вы можете проходить через нее.

После получения контейнера с ключом верхнего уровня из декодера с container(keyedBy:) (поскольку у вас есть объект 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] , где каждый элемент представляет значение для ключа "count" в вложенном JSON:

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

Вам необходимо выполнить итерацию через вложенный контейнер без ключа, получить вложенный контейнер с ключом на каждой итерации и декодировать значение для "count" . Вы можете использовать свойство count для контейнера unkeyed, чтобы предварительно выделить результирующий массив, а затем свойство 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, eg { "count": 4 } let nestedReviewCountContainer = try reviewCountContainer.nestedContainer( keyedBy: CodingKeys.ReviewsCount.self) self.reviewCounts.append( try nestedReviewCountContainer.decode(Int.self, forKey: .count) ) } } } 

Эти парни уже ответили на мой вопрос, но я просто подумал, что я бы разместил эту ссылку здесь, что делает это намного проще -> https://app.quicktype.io/#l=swift

Просто опубликуйте ответ JSON в левой панели и посмотрите, как ваши модели будут сгенерированы справа. Это может помочь, когда вы только начинаете.

Также вы можете использовать библиотеку 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) } } 
  • Как использовать пользовательские ключи с протоколом Swift 4 Decodable?
  • Давайте будем гением компьютера.