Aquí está mi JSON
{
"id": 1,
"user": {
"user_name": "Tester",
"real_info": {
"full_name":"Jon Doe"
}
},
"reviews_count": [
{
"count": 4
}
]
}
Aquí está la estructura en la que quiero que se guarde (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?
}
}
He consultado la documentación de Apple sobre la decodificación de estructuras anidadas, pero todavía no entiendo cómo hacer los diferentes niveles de JSON correctamente. Cualquier ayuda será muy apreciada.
Encodable
para laServerResponse
estructura siguiendo el mismo enfoque. ¿Es siquiera posible?ServerResponse
tiene menos datos queRawServerResponse
. Puede capturar laRawServerResponse
instancia, actualizarla con propiedades deServerResponse
y luego generar el JSON a partir de eso. Puede obtener una mejor ayuda publicando una nueva pregunta con el problema específico al que se enfrenta.Para resolver su problema, puede dividir su
RawServerResponse
implementación en varias partes lógicas (usando Swift 5).# 1. Implementar las propiedades y las claves de codificación necesarias
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. Establecer la estrategia de decodificación para la
id
propiedadextension 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. Establecer la estrategia de decodificación para la
userName
propiedadextension 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. Establecer la estrategia de decodificación para la
fullName
propiedadextension 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. Establecer la estrategia de decodificación para la
reviewCount
propiedadextension 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 } }
Implementación 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 */
fuente
struct
usarenum
con llaves. que es mucho más elegante 👍En lugar de tener una gran
CodingKeys
enumeración con todas las claves que necesitará para decodificar el JSON, le recomendaría dividir las claves para cada uno de sus objetos JSON anidados, utilizando enumeraciones anidadas para preservar la jerarquía:// 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 } }
Esto facilitará el seguimiento de las claves en cada nivel de su JSON.
Ahora, teniendo en cuenta que:
Un contenedor con clave se usa para decodificar un objeto JSON y se decodifica con un
CodingKey
tipo conforme (como los que hemos definido anteriormente).Un contenedor no codificado se usa para decodificar una matriz JSON y se decodifica secuencialmente (es decir, cada vez que llama a un método de decodificación o contenedor anidado en él, avanza al siguiente elemento de la matriz). Vea la segunda parte de la respuesta para saber cómo puede iterar a través de uno.
Después de obtener su contenedor con clave de nivel superior del decodificador con
container(keyedBy:)
(ya que tiene un objeto JSON en el nivel superior), puede usar repetidamente los métodos:nestedContainer(keyedBy:forKey:)
para obtener un objeto anidado de un objeto para una clave determinadanestedUnkeyedContainer(forKey:)
para obtener una matriz anidada de un objeto para una clave determinadanestedContainer(keyedBy:)
para obtener el siguiente objeto anidado de una matriznestedUnkeyedContainer()
para obtener la siguiente matriz anidada de una matrizPor ejemplo:
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) } }
Ejemplo de decodificación:
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 a través de un contenedor sin clave
Teniendo en cuenta el caso en el que desea
reviewCount
ser un[Int]
, donde cada elemento representa el valor de la"count"
clave en el JSON anidado:"reviews_count": [ { "count": 4 }, { "count": 5 } ]
Deberá iterar a través del contenedor anidado sin clave, obtener el contenedor anidado con clave en cada iteración y decodificar el valor de la
"count"
clave. Puede usar lacount
propiedad del contenedor sin clave para preasignar la matriz resultante y luego laisAtEnd
propiedad para iterar a través de ella.Por ejemplo:
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) ) } } }
fuente
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
enumeración con todas las claves que necesitará para decodificar su objeto JSON, debe dividirlas en varias enumeraciones para cada objeto JSON, por ejemplo, en el código anterior que tenemosCodingKeys.User
con las claves para decodificar el objeto JSON del usuario ({ "user_name": "Tester", "real_info": { "full_name": "Jon Doe" } }
), así que solo las claves para"user_name"
&"real_info"
.reviews_count
cuál es una matriz de diccionario. Actualmente, el código funciona como se esperaba. My reviewsCount solo tiene un valor en la matriz. Pero, ¿qué pasa si realmente quisiera una matriz de review_count, entonces simplemente tendría que declararvar reviewCount: Int
como una matriz, verdad? ->var reviewCount: [Int]
. Y luego también tendría que editar laReviewsCount
enumeración, ¿verdad?Int
, sino una matriz de objetos JSON, cada uno de los cuales tiene unInt
valor para una clave determinada, por lo que lo que debe hacer es iterar el contenedor sin clave y obtenga todos los contenedores con clave anidados, decodificando unInt
para cada uno (y luego agregándolos a su matriz), por ejemplo, gist.github.com/hamishknight/9b5c202fe6d8289ee2cb9403876a1b41Ya se han publicado muchas buenas respuestas, pero hay un método más simple que aún no se ha descrito en mi opinión.
Cuando los nombres de los campos JSON se escriben con
snake_case_notation
, aún puede usarcamelCaseNotation
en su archivo Swift.Solo necesitas configurar
Después de esta línea ☝️, Swift hará coincidir automáticamente todos los
snake_case
campos del JSON con loscamelCase
campos del modelo Swift.P.ej
Aquí está el código completo
1. Escribir el 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. Configuración del decodificador
let decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase
3. Decodificación
do { let response = try? decoder.decode(Response.self, from: data) print(response) } catch { debugPrint(error) }
fuente
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)
fuente
jsonStr
, puede usar esto en lugar de los dosguard let
anteriores:guard let jsonStrData: Data? = jsonStr.data(using: .utf8)! else { print("error") }
luego conviertajsonStrData
a su estructura como se describe arriba en lalet yourObject
líneaTambién puede utilizar la biblioteca KeyedCodable que preparé. Requerirá menos código. Déjame saber lo que piensas al respecto.
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) } }
fuente