Swift 4 descifrable con claves desconocidas hasta el momento de la deencoding

¿Cómo funciona el protocolo Swift 4 Decodable con 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 variedad de diccionarios; el primero tiene las claves categoryName y Trending , mientras que el segundo tiene las 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 defines la propiedad CodingKeys . Si bien es más comúnmente una enum , puede ser cualquier cosa que se ajuste al protocolo CodingKey . Y para hacer 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 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(stringValue: "categoryName")! static func makeKey(name: String) -> CodingKeys { return CodingKeys(stringValue: name)! } } 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: .makeKey(name: 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é el nombre de isFavourit en JSON a isFavourite porque pensé que era incorrecto. 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 tal que extraiga la clave que ha especificado:

 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 de 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 es crear dos contenedores desde el decodificador, uno usando la enum estándar CodingKeys para extraer el valor de la clave "categoryName" , y otro usando 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 statement de impresión, lo que demuestra 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 que en la vida real tendríamos algún tipo de manejo de errores, sin dudas!


EDITAR Más tarde me di cuenta (en parte gracias a la respuesta de CodeDifferent) que no necesitaba dos contenedores; Puedo eliminar CodingKeys enum, ¡y mi estructura CK puede hacer todo el trabajo! Es un fabricante de llaves de uso 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 eventualmente 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 una estructura así:

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

Entonces 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 ......