Как декодировать вложенную структуру 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. Любая помощь будет высоко ценится.
9 ответов
Другой подход заключается в создании промежуточной модели, которая точно соответствует JSON (с помощью такого инструмента, как https://app.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 значения в будущем.
Чтобы решить вашу проблему, вы можете разделить 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. Установите стратегию декодирования для 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
*/
Вместо того, чтобы иметь один большой 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'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:)
(поскольку у вас есть объект 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
свойство контейнера без ключа для предварительного выделения результирующего массива, а затем 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 на 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)
Эти ребята уже ответили на мой вопрос, но я просто подумал, что выложу здесь эту ссылку, которая значительно облегчит эту задачу -> https://app.quicktype.io/
Просто опубликуйте свой ответ JSON на левой панели и посмотрите, как сгенерированы ваши модели справа. Это может помочь, когда вы только начинаете.
Уже было опубликовано много хороших ответов, но есть более простой метод, который еще не описан, 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)
}
Вам не нужно многого, как эти гигантские ответы!
Вам не нужно объяснять себя. 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
}()
Пояснительный ответ:
Рассматриваемая проблема заключается в следующем:
«Как декодировать вложенную структуру JSON с помощью Swift…»
На момент написания в 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
}
Вот и все.
Также вы можете использовать библиотеку 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)
}
}