Hier is mijn JSON
{
"id": 1,
"user": {
"user_name": "Tester",
"real_info": {
"full_name":"Jon Doe"
}
},
"reviews_count": [
{
"count": 4
}
]
}
Hier is de structuur waarin ik het wil bewaren (onvolledig)
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?
}
}
Ik heb Apple’s documentatieover het decoderen van geneste structuren bekeken, maar ik heb nog steeds begrijp niet hoe de verschillende niveaus van de JSON correct moeten worden uitgevoerd. Alle hulp wordt zeer op prijs gesteld.
Antwoord 1, autoriteit 100%
Een andere benadering is om een tussenmodel te maken dat nauw aansluit bij de JSON (met behulp van een tool als quicktype.io), laat Swift de methoden genereren om het te decoderen, en kies vervolgens de stukjes die je wilt in je uiteindelijke gegevensmodel:
// 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
}
}
Hierdoor kunt u ook gemakkelijk reviews_count
doorlopen, mocht het in de toekomst meer dan 1 waarde bevatten.
Antwoord 2, autoriteit 95%
Om uw probleem op te lossen, kunt u uw RawServerResponse
-implementatie opsplitsen in verschillende logische delen (met Swift 5).
#1. Implementeer de eigenschappen en vereiste coderingssleutels
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. Stel de decoderingsstrategie in voor de eigenschap 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. Stel de decoderingsstrategie in voor de eigenschap 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. Stel de decoderingsstrategie in voor de eigenschap 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. Stel de decoderingsstrategie in voor de eigenschap 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
}
}
Voltooide implementatie
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
}
}
Gebruik
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
*/
Antwoord 3, autoriteit 26%
In plaats van één grote CodingKeys
opsomming te hebben met allesleutels die je nodig hebt voor het decoderen van de JSON, zou ik adviseren de sleutels op te splitsen voor elkvan uw geneste JSON-objecten, met behulp van geneste opsommingen om de hiërarchie te behouden:
// 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
}
}
Dit maakt het gemakkelijker om de sleutels op elk niveau in uw JSON bij te houden.
Nu, rekening houdend met het volgende:
-
Een gecodeerde containerwordt gebruikt om een JSON-object te decoderen en is gedecodeerd met een
CodingKey
conform type (zoals degene die we heb hierboven gedefinieerd). -
Een unkeyed containerwordt gebruikt om een JSON-array te decoderen en is gedecodeerd opeenvolgend(dwz elke keer dat u een decodeer- of geneste containermethode erop aanroept, gaat deze door naar het volgende element in de array). Zie het tweede deel van het antwoord voor hoe je er een kunt herhalen.
Nadat u uw versleuteldecontainer op het hoogste niveau van de decoder hebt gehaald met container(keyedBy:)
(omdat je een JSON-object op het hoogste niveau hebt), kun je herhaaldelijk de methoden gebruiken:
nestedContainer(keyedBy:forKey:)
om een genest object uit een object te halen voor een bepaalde sleutelnestedUnkeyedContainer(forKey:)
om een geneste array van een object voor een bepaalde sleutel te krijgennestedContainer(keyedBy:)
om het volgende geneste object uit een array te halennestedUnkeyedContainer()
om de volgende geneste array uit een array
Bijvoorbeeld:
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)
}
}
Voorbeeld decodering:
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)
Itereren door een niet-gecodeerde container
Gezien het geval waarin u wilt dat reviewCount
een [Int]
is, waarbij elk element de waarde vertegenwoordigt voor de sleutel "count"
in de geneste JSON:
"reviews_count": [
{
"count": 4
},
{
"count": 5
}
]
U moet de geneste niet-gecodeerde container doorlopen, de geneste versleutelde container bij elke herhaling ophalen en de waarde voor de sleutel "count"
decoderen. U kunt de eigenschap count
van de unkeyed container om de resulterende array vooraf toe te wijzen, en vervolgens de isAtEnd
eigenschap om er doorheen te bladeren.
Bijvoorbeeld:
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)
)
}
}
}
Antwoord 4, autoriteit 5%
- Kopieer het json-bestand naar https://app.quicktype.io
- Selecteer Swift (als je Swift 5 gebruikt, controleer dan de compatibiliteitsschakelaar voor Swift 5)
- Gebruik de volgende code om het bestand te decoderen
- Voila!
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)
Antwoord 5, autoriteit 5%
Er zijn al veel goede antwoorden gepost, maar er is een eenvoudigere methode die IMO nog niet is beschreven.
Als de JSON-veldnamen zijn geschreven met snake_case_notation
, kun je nog steeds de camelCaseNotation
in je Swift-bestand gebruiken.
Je hoeft alleen maar in te stellen
decoder.keyDecodingStrategy = .convertFromSnakeCase
Na deze ☝️-regel zal Swift automatisch alle snake_case
-velden van de JSON matchen met de camelCase
-velden in het Swift-model.
Bijvoorbeeld
user_name` -> userName
reviews_count -> `reviewsCount
...
Hier is de volledige code
1. Het model schrijven
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. De decoder instellen
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
3. Decodering
do {
let response = try? decoder.decode(Response.self, from: data)
print(response)
} catch {
debugPrint(error)
}
Antwoord 6
U kunt ook de bibliotheek KeyedCodablegebruiken die ik heb voorbereid. Er is minder code nodig. Laat me weten wat je ervan vindt.
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)
}
}