Aqui está meu JSON
{
"id": 1,
"user": {
"user_name": "Tester",
"real_info": {
"full_name":"Jon Doe"
}
},
"reviews_count": [
{
"count": 4
}
]
}
Aqui está a estrutura que eu quero salvar (incompleta)
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?
}
}
Eu olhei a documentação da Apple sobre a decodificação de structs aninhados, mas ainda não entendi como fazer os diferentes níveis do JSON corretamente. Qualquer ajuda será muito apreciada.
Encodable
aServerResponse
estrutura seguindo a mesma abordagem. É mesmo possível?ServerResponse
tem menos dados do queRawServerResponse
. Você pode capturar aRawServerResponse
instância, atualizá-la com propriedades de eServerResponse
, em seguida, gerar o JSON a partir disso. Você pode obter ajuda melhor postando uma nova pergunta com o problema específico que está enfrentando.Para resolver seu problema, você pode dividir sua
RawServerResponse
implementação em várias partes lógicas (usando Swift 5).# 1. Implementar as propriedades e as chaves de codificação necessárias
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. Defina a estratégia de decodificação para
id
propriedadeextension 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. Defina a estratégia de decodificação da
userName
propriedadeextension 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. Defina a estratégia de decodificação da
fullName
propriedadeextension 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. Defina a estratégia de decodificação para
reviewCount
propriedadeextension 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 } }
Implementação completa
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 } }
Uso
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 */
fonte
struct
você usarenum
com as chaves. que é muito mais elegante 👍Em vez de ter uma grande
CodingKeys
enumeração com todas as chaves de que você precisará para decodificar o JSON, aconselho dividir as chaves para cada um de seus objetos JSON aninhados, usando enumerações aninhadas para preservar a hierarquia:// 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 } }
Isso tornará mais fácil controlar as chaves em cada nível do JSON.
Agora, tendo em mente que:
Um contêiner com chave é usado para decodificar um objeto JSON e é decodificado com um
CodingKey
tipo em conformidade (como os que definimos acima).Um contêiner sem chave é usado para decodificar uma matriz JSON e é decodificado sequencialmente (ou seja, cada vez que você chama um método de decodificação ou contêiner aninhado nele, ele avança para o próximo elemento na matriz). Veja a segunda parte da resposta para saber como você pode iterar em um.
Depois de obter seu contêiner codificado de nível superior do decodificador com
container(keyedBy:)
(já que você tem um objeto JSON no nível superior), você pode usar os métodos repetidamente:nestedContainer(keyedBy:forKey:)
obter um objeto aninhado de um objeto para uma determinada chavenestedUnkeyedContainer(forKey:)
para obter uma matriz aninhada de um objeto para uma determinada chavenestedContainer(keyedBy:)
para obter o próximo objeto aninhado de uma matriznestedUnkeyedContainer()
para obter o próximo array aninhado de um arrayPor exemplo:
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) } }
Exemplo de decodificação:
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)
Iterando por meio de um contêiner sem chave
Considerando o caso em que você deseja
reviewCount
ser um[Int]
, em que cada elemento representa o valor da"count"
chave no JSON aninhado:"reviews_count": [ { "count": 4 }, { "count": 5 } ]
Você precisará iterar pelo contêiner aninhado sem chave, obtendo o contêiner aninhado com chave em cada iteração e decodificando o valor da
"count"
chave. Você pode usar acount
propriedade do contêiner sem chave para pré-alocar a matriz resultante e, em seguida, aisAtEnd
propriedade para iterar por meio dela.Por exemplo:
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) ) } } }
fonte
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
enum com todas as chaves que você precisará para decodificar seu objeto JSON, você deve dividi-los em vários enums para cada objeto JSON - por exemplo, no código acima que temosCodingKeys.User
com as chaves para decodificar o objeto JSON do usuário ({ "user_name": "Tester", "real_info": { "full_name": "Jon Doe" } }
), então apenas as chaves para"user_name"
&"real_info"
.reviews_count
que é um dicionário. Atualmente, o código funciona conforme o esperado. Meu reviewsCount sempre tem apenas um valor na matriz. Mas e se eu realmente quisesse um array de review_count, então precisaria simplesmente declararvar reviewCount: Int
como um array, certo? ->var reviewCount: [Int]
. E então eu preciso editar oReviewsCount
enum, certo?Int
, mas uma matriz de objetos JSON em que cada um tem umInt
valor para uma determinada chave - então o que você precisa fazer é iterar através o contêiner sem chave e obter todos os contêineres com chave aninhados, decodificando umInt
para cada um (e depois anexando-os à sua matriz), por exemplo, gist.github.com/hamishknight/9b5c202fe6d8289ee2cb9403876a1b41Muitas boas respostas já foram postadas, mas existe um método mais simples ainda não descrito na IMO.
Quando os nomes de campo JSON são gravados usando,
snake_case_notation
você ainda pode usarcamelCaseNotation
no seu arquivo Swift.Você só precisa definir
Após essa ☝️ linha, o Swift fará a correspondência automática de todos os
snake_case
campos do JSON com oscamelCase
campos do modelo Swift.Por exemplo
Aqui está o código completo
1. Escrevendo o modelo
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. Configurando o decodificador
let decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase
3. Decodificação
do { let response = try? decoder.decode(Response.self, from: data) print(response) } catch { debugPrint(error) }
fonte
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)
fonte
jsonStr
, você pode usar isso em vez dos doisguard let
s acima:guard let jsonStrData: Data? = jsonStr.data(using: .utf8)! else { print("error") }
então convertajsonStrData
para sua estrutura conforme descrito acima nalet yourObject
linhaVocê também pode usar a biblioteca KeyedCodable que preparei. Exigirá menos código. Deixe-me saber o que você pensa sobre isso.
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) } }
fonte