Как декодировать вложенную структуру 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 Documentation об декодировании вложенных структур, но я до сих пор не понимаю, как правильно выполнять различные уровни JSON. Любая помощь будет высоко оценена.
Ответы
Ответ 1
Другой подход заключается в создании промежуточной модели, которая точно соответствует JSON (с помощью такого инструмента, как quicktype.io), пусть Swift генерирует методы для его декодирования, а затем отбирает фрагменты, которые вы хотите в своей окончательной модели данных:
// 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 значения.
Ответ 2
Чтобы решить вашу проблему, вы можете разделить реализацию 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
*/
Ответ 3
Вместо того, чтобы иметь одно большое перечисление CodingKeys
со всеми ключами, необходимыми для декодирования JSON, я бы посоветовал разделить ключи для каждого из ваших вложенных объектов 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 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.
Теперь, имея в виду, что:
-
A keyed container используется для декодирования объекта JSON и декодируется с помощью CodingKey
соответствующий тип (например, те, которые мы определили выше).
-
unkeyed container используется для декодирования массива JSON и декодируется последовательно (т.е. каждый раз, когда вы вызываете декодирование или вложенный контейнер на нем, он переходит к следующему элементу массива). См. Вторую часть ответа о том, как вы можете проходить через один.
После получения контейнера с ключом верхнего уровня из декодера с 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]
, где каждый элемент представляет значение для ключа "count"
вложенного JSON:
"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)
)
}
}
}
Ответ 4
Эти ребята уже ответили на мой вопрос, но я просто подумал, что выложу здесь эту ссылку, которая значительно облегчит эту задачу → https://app.quicktype.io/#l=swift
Просто опубликуйте свой ответ JSON на левой панели и посмотрите, как сгенерированы ваши модели справа. Это может помочь, когда вы только начинаете.
Ответ 5
Также вы можете использовать библиотеку 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)
}
}
Ответ 6
Многие хорошие ответы уже были опубликованы, но есть более простой метод, который еще не описан в IMO.
Когда имена полей JSON пишутся с использованием snake_case_notation
вы все равно можете использовать camelCaseNotation
в вашем файле Swift.
Вам просто нужно установить
decoder.keyDecodingStrategy = .convertFromSnakeCase
После этой ☝️ строки Swift автоматически сопоставит все поля snake_case
из JSON с полями camelCase
в модели Swift.
Например
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)
}