Вот мой 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. Любая помощь будет высоко ценится.
Encodable
дляServerResponse
структуры следующего такого же подхода. Это вообще возможно?ServerResponse
что данных меньше, чемRawServerResponse
. Вы можете захватитьRawServerResponse
экземпляр, обновить его свойствами изServerResponse
, а затем сгенерировать из него JSON. Вы можете получить лучшую помощь, разместив новый вопрос с конкретной проблемой, с которой вы столкнулись.Чтобы решить вашу проблему, вы можете разбить вашу
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 */
источник
struct
чтобы использоватьenum
с ключами. что намного элегантнее 👍Вместо того, чтобы иметь одно большое
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
соответствующим типом (например , как те , мы определили выше).Unkeyed контейнер используется для декодирования массива 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) ) } } }
источник
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
перечисление со всеми ключами, которые вам понадобятся для декодирования вашего объекта JSON, вы должны разделить их на несколько перечислений для каждого объекта JSON - например, в приведенном выше коде у нас естьCodingKeys.User
ключи для декодирования пользовательского объекта JSON ({ "user_name": "Tester", "real_info": { "full_name": "Jon Doe" } }
), поэтому просто ключи для"user_name"
&"real_info"
.reviews_count
что представляет собой массив словаря. В настоящее время код работает должным образом. My reviewsCount всегда имеет только одно значение в массиве. Но что, если мне действительно нужен массив review_count, тогда мне нужно будет просто объявитьvar reviewCount: Int
как массив, правильно? ->var reviewCount: [Int]
. И тогда мне нужно будет также отредактироватьReviewsCount
перечисление, верно?Int
, а массив объектов JSON, каждый из которых имеетInt
значение для данного ключа, поэтому вам нужно выполнить итерацию контейнер без ключа и получите все вложенные контейнеры с ключами, декодируяInt
для каждого из них (а затем добавляя их в свой массив), например gist.github.com/hamishknight/9b5c202fe6d8289ee2cb9403876a1b41Уже было опубликовано много хороших ответов, но есть более простой метод, который еще не описан, IMO.
Когда имена полей JSON написаны с использованием,
snake_case_notation
вы все равно можете использоватьcamelCaseNotation
в своем файле Swift.Вам просто нужно установить
После этой строки Swift автоматически сопоставит все
snake_case
поля из JSON сcamelCase
полями в модели Swift.Например
Вот полный код
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) }
источник
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
s выше: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) } }
источник