jsonserialization jsondecoder json swift swift4 codable

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:

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) }