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