jsondecoder - jsonserialization swift 4
¿Cómo decodificar una estructura JSON anidada con el protocolo Swift Decodable? (6)
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 guardarla (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 visto la documentación de Apple sobre decodificación de estructuras anidadas, pero todavía no entiendo cómo hacer los diferentes niveles de JSON correctamente. Cualquier ayuda será muy apreciada.
En lugar de tener una gran enumeración
CodingKeys
con
todas
las claves que necesitará para decodificar el JSON, 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 en su JSON.
Ahora, teniendo en cuenta que:
-
Un contenedor con clave se utiliza para decodificar un objeto JSON, y se decodifica con un tipo conforme de
CodingKey
(como los que hemos definido anteriormente). -
Se utiliza un contenedor sin clave para decodificar una matriz JSON y se decodifica secuencialmente (es decir, cada vez que se llama a un método de decodificación o contenedor anidado, avanza al siguiente elemento de la matriz). Vea la segunda parte de la respuesta para saber cómo puede recorrer 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 determinada -
nestedUnkeyedContainer(forKey:)
para obtener una matriz anidada de un objeto para una clave determinada -
nestedContainer(keyedBy:)
para obtener el siguiente objeto anidado de una matriz -
nestedUnkeyedContainer()
para obtener la siguiente matriz anidada de una matriz
Por 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 que
reviewCount
sea un
[Int]
, donde cada elemento representa el valor de la clave
"count"
en el JSON anidado:
"reviews_count": [
{
"count": 4
},
{
"count": 5
}
]
Tendrá que recorrer el contenedor anidado sin clave, obtener el contenedor anidado con clave en cada iteración y decodificar el valor de la clave
"count"
.
Puede usar la propiedad de conteo del contenedor sin clave para preasignar la matriz resultante, y luego la propiedad
isAtEnd
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)
)
}
}
}
Estos chicos ya respondieron mi pregunta, pero pensé que publicaría este enlace aquí, lo que hace que esto sea mucho más fácil -> https://app.quicktype.io/#l=swift
Simplemente publique su respuesta JSON en el panel izquierdo y observe cómo se generan sus Modelos a la derecha. Esto puede ayudar cuando recién está comenzando.
Otro enfoque es crear un modelo intermedio que coincida estrechamente con el JSON (con la ayuda de una herramienta como quicktype.io ), dejar que Swift genere los métodos para decodificarlo y luego seleccionar las piezas que desee en su modelo de datos final:
// 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
}
}
Esto también le permite iterar fácilmente a través de
reviews_count
, en caso de que contenga más de 1 valor en el futuro.
Para resolver su problema, puede dividir su implementación de
RawServerResponse
en varias partes lógicas (usando Swift 5).
# 1 Implemente 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 propiedad
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
Establecer la estrategia de decodificación para la propiedad
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.
Establecer la estrategia de decodificación para la propiedad
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.
Establecer la estrategia de decodificación para la propiedad
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
}
}
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
*/
También puede usar 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)
}
}
Ya se han publicado muchas buenas respuestas, pero hay un método más simple que aún no se describe en la OMI.
Cuando los nombres de campo JSON se escriben usando
snake_case_notation
, aún puede usar
camelCaseNotation
en su archivo Swift.
Solo necesitas configurar
decoder.keyDecodingStrategy = .convertFromSnakeCase
Después de esta línea ☝️, Swift combinará automáticamente todos los campos
snake_case
del JSON con los campos
camelCase
en el modelo Swift.
P.ej
user_name` -> userName
reviews_count -> `reviewsCount
...
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)
}