jsonserialization json swift4 decodable

jsonserialization - Swift 4 Decodificable con claves desconocidas hasta el momento de la decodificación



jsonserialization swift 4 (3)

¿Cómo hace frente el protocolo Swift 4 Decodable a un diccionario que contiene una clave cuyo nombre no se conoce hasta el tiempo de ejecución? Por ejemplo:

[ { "categoryName": "Trending", "Trending": [ { "category": "Trending", "trailerPrice": "", "isFavourit": null, "isWatchlist": null } ] }, { "categoryName": "Comedy", "Comedy": [ { "category": "Comedy", "trailerPrice": "", "isFavourit": null, "isWatchlist": null } ] } ]

Aquí tenemos una gran variedad de diccionarios; el primero tiene claves categoryName y Trending , mientras que el segundo tiene claves categoryName y Comedy . El valor de la clave categoryName me dice el nombre de la segunda clave. ¿Cómo expreso eso usando Decodable?


La clave está en cómo define la propiedad CodingKeys . Si bien es más comúnmente una enum , puede ser cualquier cosa que se ajuste al protocolo CodingKey . Y para crear claves dinámicas, puede llamar a una función estática:

struct Category: Decodable { struct Detail: Decodable { var category: String var trailerPrice: String var isFavorite: Bool? var isWatchlist: Bool? } var name: String var detail: Detail private struct CodingKeys: CodingKey { var intValue: Int? var stringValue: String init?(intValue: Int) { self.intValue = intValue; self.stringValue = "/(intValue)" } init?(stringValue: String) { self.stringValue = stringValue } static let name = CodingKeys.make(key: "categoryName") static func make(key: String) -> CodingKeys { return CodingKeys(stringValue: key)! } } init(from coder: Decoder) throws { let container = try coder.container(keyedBy: CodingKeys.self) self.name = try container.decode(String.self, forKey: .name) self.detail = try container.decode([Detail].self, forKey: .make(key: name)).first! } }

Uso:

let jsonData = """ [ { "categoryName": "Trending", "Trending": [ { "category": "Trending", "trailerPrice": "", "isFavourite": null, "isWatchlist": null } ] }, { "categoryName": "Comedy", "Comedy": [ { "category": "Comedy", "trailerPrice": "", "isFavourite": null, "isWatchlist": null } ] } ] """.data(using: .utf8)! let categories = try! JSONDecoder().decode([Category].self, from: jsonData)

(Cambié isFavourit en JSON a isFavourite porque pensé que era un isFavourite . Es bastante fácil adaptar el código si ese no es el caso)


Puede escribir una estructura personalizada que funcione como un objeto CodingKeys e inicializarla con una cadena de manera que extraiga la clave que especificó:

private struct CK : CodingKey { var stringValue: String init?(stringValue: String) { self.stringValue = stringValue } var intValue: Int? init?(intValue: Int) { return nil } }

Por lo tanto, una vez que sepa cuál es la clave deseada, puede decir (en la anulación init(from:) :

let key = // whatever the key name turns out to be let con2 = try! decoder.container(keyedBy: CK.self) self.unknown = try! con2.decode([Inner].self, forKey: CK(stringValue:key)!)

Entonces, lo que terminé haciendo fue hacer dos contenedores desde el decodificador: uno con la enumeración estándar de CodingKeys para extraer el valor de la clave "categoryName" y otro con la estructura CK para extraer el valor de la clave cuyo nombre acabamos de aprender:

init(from decoder: Decoder) throws { let con = try! decoder.container(keyedBy: CodingKeys.self) self.categoryName = try! con.decode(String.self, forKey:.categoryName) let key = self.categoryName let con2 = try! decoder.container(keyedBy: CK.self) self.unknown = try! con2.decode([Inner].self, forKey: CK(stringValue:key)!) }

Aquí, entonces, está toda mi estructura Decodable:

struct ResponseData : Codable { let categoryName : String let unknown : [Inner] struct Inner : Codable { let category : String let trailerPrice : String let isFavourit : String? let isWatchList : String? } private enum CodingKeys : String, CodingKey { case categoryName } private struct CK : CodingKey { var stringValue: String init?(stringValue: String) { self.stringValue = stringValue } var intValue: Int? init?(intValue: Int) { return nil } } init(from decoder: Decoder) throws { let con = try! decoder.container(keyedBy: CodingKeys.self) self.categoryName = try! con.decode(String.self, forKey:.categoryName) let key = self.categoryName let con2 = try! decoder.container(keyedBy: CK.self) self.unknown = try! con2.decode([Inner].self, forKey: CK(stringValue:key)!) } }

Y aquí está el banco de pruebas:

let json = """ [ { "categoryName": "Trending", "Trending": [ { "category": "Trending", "trailerPrice": "", "isFavourit": null, "isWatchlist": null } ] }, { "categoryName": "Comedy", "Comedy": [ { "category": "Comedy", "trailerPrice": "", "isFavourit": null, "isWatchlist": null } ] } ] """ let myjson = try! JSONDecoder().decode( [ResponseData].self, from: json.data(using: .utf8)!) print(myjson)

Y aquí está el resultado de la declaración de impresión, demostrando que hemos poblado nuestras estructuras correctamente:

[JustPlaying.ResponseData( categoryName: "Trending", unknown: [JustPlaying.ResponseData.Inner( category: "Trending", trailerPrice: "", isFavourit: nil, isWatchList: nil)]), JustPlaying.ResponseData( categoryName: "Comedy", unknown: [JustPlaying.ResponseData.Inner( category: "Comedy", trailerPrice: "", isFavourit: nil, isWatchList: nil)]) ]

Por supuesto, en la vida real tendríamos que manejar algunos errores, ¡sin duda!

EDITAR Más tarde me di cuenta (en parte gracias a la respuesta de CodeDifferent) de que no necesitaba dos contenedores; ¡Puedo eliminar la enumeración de CodingKeys, y mi estructura CK puede hacer todo el trabajo! Es un creador de claves de propósito general:

init(from decoder: Decoder) throws { let con = try! decoder.container(keyedBy: CK.self) self.categoryName = try! con.decode(String.self, forKey:CK(stringValue:"categoryName")!) let key = self.categoryName self.unknown = try! con.decode([Inner].self, forKey: CK(stringValue:key)!) }


también, hizo esta pregunta. Esto es lo que finalmente surgió para este json:

let json = """ { "BTC_BCN":{ "last":"0.00000057", "percentChange":"0.03636363", "baseVolume":"47.08463318" }, "BTC_BELA":{ "last":"0.00001281", "percentChange":"0.07376362", "baseVolume":"5.46595029" } } """.data(using: .utf8)!

Hacemos tal estructura:

struct Pair { let name: String let details: Details struct Details: Codable { let last, percentChange, baseVolume: String } }

Luego decodificar:

if let pairsDictionary = try? JSONDecoder().decode([String: Pair.Details].self, from: json) { var pairs: [Pair] = [] for (name, details) in pairsDictionary { let pair = Pair(name: name, details: details) pairs.append(pair) } print(pairs) }

También es posible llamar no pair.details.baseVolume, sino pair.baseVolume:

struct Pair { ...... var baseVolume: String { return details.baseVolume } ......

O escriba init personalizado:

struct Pair { ..... let baseVolume: String init(name: String, details: Details) { self.baseVolume = details.baseVolume ......