Oto mój JSON
{
"id": 1,
"user": {
"user_name": "Tester",
"real_info": {
"full_name":"Jon Doe"
}
},
"reviews_count": [
{
"count": 4
}
]
}
Oto struktura, w której chcę, aby została zapisana (niekompletna)
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?
}
}
Spojrzałem na dokumentację Apple dotyczącą dekodowania zagnieżdżonych struktur, ale nadal nie rozumiem, jak poprawnie wykonać różne poziomy JSON. Każda pomoc będzie mile widziana.
Encodable
wServerResponse
strukturze, stosując to samo podejście. Czy to w ogóle możliwe?ServerResponse
ma mniej danych niżRawServerResponse
. Możesz przechwycićRawServerResponse
instancję, zaktualizować ją za pomocą właściwości zServerResponse
, a następnie wygenerować na jej podstawie JSON. Możesz uzyskać lepszą pomoc, publikując nowe pytanie dotyczące konkretnego problemu, z którym się borykasz.Aby rozwiązać problem, możesz podzielić swoją
RawServerResponse
implementację na kilka części logicznych (używając Swift 5).# 1. Zaimplementuj właściwości i wymagane klucze kodowania
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. Ustaw strategię dekodowania dla
id
właściwościextension 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. Ustaw strategię dekodowania dla
userName
właściwościextension 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. Ustaw strategię dekodowania dla
fullName
właściwościextension 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. Ustaw strategię dekodowania dla
reviewCount
właściwościextension 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 } }
Pełne wdrożenie
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 } }
Stosowanie
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 */
źródło
struct
używaćenum
z kluczami. co jest o wiele bardziej eleganckie 👍Zamiast mieć jedno duże
CodingKeys
wyliczenie ze wszystkimi kluczami potrzebnymi do dekodowania JSON, radziłbym podzielić klucze dla każdego z zagnieżdżonych obiektów JSON, używając zagnieżdżonych wyliczeń, aby zachować hierarchię:// 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 } }
Ułatwi to śledzenie kluczy na każdym poziomie w JSON.
Mając na uwadze, że:
Wpust Pojemnik służy do dekodowania obiektów JSON i jest dekodowany z
CodingKey
rodzaju odpowiadającego (na przykład te, które zostały zdefiniowane powyżej).Unkeyed pojemnik służy do dekodowania tablicę JSON i jest dekodowany sekwencyjnie (czyli za każdym razem można nazwać dekodowania lub zagnieżdżonych metody pojemnik na to, że przejście do następnego elementu w tablicy). Zobacz drugą część odpowiedzi, aby dowiedzieć się, jak przejść przez jedną z nich.
Po pobraniu kontenera z kluczem najwyższego poziomu z dekodera za pomocą
container(keyedBy:)
(ponieważ masz obiekt JSON na najwyższym poziomie), możesz wielokrotnie używać tych metod:nestedContainer(keyedBy:forKey:)
aby uzyskać zagnieżdżony obiekt z obiektu dla danego kluczanestedUnkeyedContainer(forKey:)
aby pobrać zagnieżdżoną tablicę z obiektu dla danego kluczanestedContainer(keyedBy:)
aby pobrać następny zagnieżdżony obiekt z tablicynestedUnkeyedContainer()
aby pobrać następną zagnieżdżoną tablicę z tablicyNa przykład:
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) } }
Przykładowe dekodowanie:
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)
Iterowanie przez kontener bez klucza
Biorąc pod uwagę przypadek, w którym chcesz
reviewCount
być an[Int]
, gdzie każdy element reprezentuje wartość"count"
klucza w zagnieżdżonym formacie JSON:"reviews_count": [ { "count": 4 }, { "count": 5 } ]
Będziesz musiał wykonać iterację przez zagnieżdżony kontener bez klucza, uzyskać zagnieżdżony kontener z kluczem w każdej iteracji i zdekodować wartość
"count"
klucza. Możesz użyćcount
właściwości kontenera bez klucza, aby wstępnie przydzielić wynikową tablicę, a następnieisAtEnd
właściwość do jej iteracji.Na przykład:
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) ) } } }
źródło
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
wyliczenia ze wszystkimi kluczami potrzebnymi do zdekodowania obiektu JSON, należy je podzielić na wiele wyliczeń dla każdego obiektu JSON - na przykład w powyższym kodzie, który mamyCodingKeys.User
z kluczami aby zdekodować obiekt JSON użytkownika ({ "user_name": "Tester", "real_info": { "full_name": "Jon Doe" } }
), więc tylko klucze"user_name"
&"real_info"
.reviews_count
które jest tablicą słowników. Obecnie kod działa zgodnie z oczekiwaniami. My reviewsCount zawsze ma tylko jedną wartość w tablicy. Ale co, jeśli rzeczywiście chciałbym mieć tablicę review_count, to musiałbym po prostu zadeklarowaćvar reviewCount: Int
jako tablicę, prawda? ->var reviewCount: [Int]
. A potem musiałbym również edytowaćReviewsCount
wyliczenie, prawda?Int
, ale tablica obiektów JSON, z których każdy maInt
wartość dla danego klucza - więc to, co musisz zrobić, to iterować bezkluczowy kontener i pobierz wszystkie zagnieżdżone kontenery z kluczem, dekodującInt
każdy z nich (a następnie dodając je do swojej tablicy), np. gist.github.com/hamishknight/9b5c202fe6d8289ee2cb9403876a1b41Wiele dobrych odpowiedzi zostało już opublikowanych, ale istnieje prostsza metoda, jeszcze nie opisana IMO.
Gdy nazwy pól JSON są zapisywane przy użyciu
snake_case_notation
, możesz nadal używaćcamelCaseNotation
w swoim pliku Swift.Musisz tylko ustawić
Po tym ☝️ wierszu Swift automatycznie dopasuje wszystkie
snake_case
pola z JSON docamelCase
pól w modelu Swift.Na przykład
Oto pełny kod
1. Pisanie modelu
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. Ustawianie dekodera
let decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase
3. Dekodowanie
do { let response = try? decoder.decode(Response.self, from: data) print(response) } catch { debugPrint(error) }
źródło
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)
źródło
jsonStr
, możesz użyć tego zamiast dwóchguard let
powyższych:guard let jsonStrData: Data? = jsonStr.data(using: .utf8)! else { print("error") }
następnie przekonwertujjsonStrData
na swoją strukturę, jak opisano powyżej wlet yourObject
liniiMożesz także skorzystać z przygotowanej przeze mnie biblioteki KeyedCodable . Będzie to wymagało mniej kodu. Daj mi znać, co o tym myślisz.
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) } }
źródło