Tengo algo de JSON que me gustaría decodificar con un JSONDecoder . El problema es que el nombre de una de las propiedades es útilmente dinámico cuando se envía desde el servidor.
Me gusta esto:
{ "someRandomName": [ [1,2,3], [4,5,6] ], "staticName": 12345 } ¿Cómo puedo decodificar esto, cuando someRandomName no se conoce en el momento de la compilación? He estado navegando a través de la www en busca de una respuesta, pero todavía no hay alegría. Realmente no puedo entender cómo funciona este Decodable , CodingKey . Algunos de los ejemplos tienen docenas de líneas, ¡y eso no parece correcto!
EDITAR Debo señalar que la clave se conoce en tiempo de ejecución, por lo que tal vez pueda pasarla al decodificar el objeto.
¿Hay alguna forma de conectarse a uno de los métodos o propiedades del protocolo para habilitar esta decodificación? No me importa si tengo que escribir un decodificador a medida solo para este objeto: todos los demás JSON están bien y son estándar.
Ok, mi entendimiento me ha llevado hasta aquí:
struct Pair: Decodable { var pair: [[Double]] var last: Int private struct CodingKeys: CodingKey { var stringValue: String init?(stringValue: String) { self.stringValue = stringValue } // Use for integer-keyed dictionary var intValue: Int? init?(intValue: Int) { // We are not using this, thus just return nil return nil } } init(from decoder: Decoder) throws { // just to stop the compiler moaning pair = [[]] last = 0 let container = try decoder.container(keyedBy: CodingKeys.self) // how do I generate the key for the correspond "pair" property here? for key in container.allKeys { last = try container.decode(Int.self, forKey: CodingKeys(stringValue: "last")!) pair = try container.decode([[Double]].self, forKey: CodingKeys(stringValue: key.stringValue)!) } } } init() { let jsonString = """ { "last": 123456, "XBTUSD": [ [1.0, 2.0, 3.0], [4.0, 5.0, 6.0] ] } """ let jsonData = Data(jsonString.utf8) // this gives: "Fatal error: 'try!' expression unexpectedly raised an error: Swift.DecodingError.typeMismatch(Swift.Array<Any>, Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "last", intValue: nil)], debugDescription: "Expected to decode Array<Any> but found a number instead.", underlyingError: nil))" let decodedResult = try! JSONDecoder().decode(Pair.self, from: jsonData) dump(decodedResult) } Así que ahora entiendo que la conformidad CodingKey está generando las claves para los datos serializados, no la estructura de Swift (que tiene mucho sentido ahora que lo pienso).
Entonces, ¿cómo genero ahora el caso para pair sobre la marcha, en lugar de codificarlo de esta manera? Sé que tiene algo que ver con el init(from decoder: Decoder) que necesito implementar, pero por mi vida no puedo entender cómo funciona eso. ¡Por favor ayuda!
Ok, estoy tan cerca ahora. La decodificación parece estar funcionando con esto:
struct Pair: Decodable { var pair: [[Double]] var last: Int private enum CodingKeys : String, CodingKey { case last } private struct DynamicCodingKeys: CodingKey { var stringValue: String init?(stringValue: String) { self.stringValue = stringValue } // Use for integer-keyed dictionary var intValue: Int? init?(intValue: Int) { // We are not using this, thus just return nil return nil } } init(from decoder: Decoder) throws { // just to stop the compiler moaning pair = [[]] last = 0 let container1 = try decoder.container(keyedBy: CodingKeys.self) last = try container1.decode(Int.self, forKey: .last) let container2 = try decoder.container(keyedBy: DynamicCodingKeys.self) for key in container2.allKeys { pair = try container2.decode([[Double]].self, forKey: DynamicCodingKeys(stringValue: key.stringValue)!) } } } Este código parece hacer su trabajo: examinar las propiedades last y pair en la función en sí y se ve bien; pero recibo un error al intentar decodificar:
init() { let jsonString = """ { "last": 123456, "XBTUSD": [ [1.0, 2.0, 3.0], [4.0, 5.0, 6.0] ] } """ let jsonData = Data(jsonString.utf8) // Fatal error: 'try!' expression unexpectedly raised an error: Swift.DecodingError.typeMismatch(Swift.Array<Any>, Swift.DecodingError.Context(codingPath: [DynamicCodingKeys(stringValue: "last", intValue: nil)], debugDescription: "Expected to decode Array<Any> but found a number instead." let decodedResult = try! JSONDecoder().decode(Pair.self, from: jsonData) dump(decodedResult) }Estoy tan cerca que puedo saborearlo...
Está buscando JSONSerializer no JSONDecoder , supongo, https://developer.apple.com/documentation/foundation/jsonserialization .
Debido a que la clave es impredecible, es mejor convertirla a Dictionary . O puede echar un vistazo a este https://swiftsenpai.com/swift/decode-dynamic-keys-json/
Si la clave dinámica se conoce en tiempo de ejecución, puede pasarla a través del diccionario de información de userInfo del decodificador.
En primer lugar crea dos extensiones.
extension CodingUserInfoKey { static let dynamicKey = CodingUserInfoKey(rawValue: "dynamicKey")! } extension JSONDecoder { convenience init(dynamicKey: String) { self.init() self.userInfo[.dynamicKey] = dynamicKey } } En la estructura, implemente CodingKeys como estructura para poder crear claves sobre la marcha.
struct Pair : Decodable { let last : Int let pair : [[Double]] private struct CodingKeys: CodingKey { var intValue: Int? var stringValue: String init?(stringValue: String) { self.stringValue = stringValue } init?(intValue: Int) { self.stringValue = String(intValue) self.intValue = intValue } static let last = CodingKeys(stringValue: "last")! static func makeKey(name: String) -> CodingKeys { return CodingKeys(stringValue: name)! } } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) guard let dynamicKey = decoder.userInfo[.dynamicKey] as? String else { throw DecodingError.dataCorruptedError(forKey: .makeKey(name: "pair"), in: container, debugDescription: "Dynamic key in userInfo is missing") } last = try container.decode(Int.self, forKey: .last) pair = try container.decode([[Double]].self, forKey: .makeKey(name: dynamicKey)) } } Ahora cree el JSONDecoder pasando el nombre dinámico conocido
let jsonString = """ { "last": 123456, "XBTUSD": [ [1.0, 2.0, 3.0], [4.0, 5.0, 6.0] ] } """ do { let decoder = JSONDecoder(dynamicKey: "XBTUSD") let result = try decoder.decode(Pair.self, from: Data(jsonString.utf8)) print(result) } catch { print(error) }Editar:
Si el JSON contiene siempre solo dos claves, este es un enfoque más fácil:
struct AnyKey: CodingKey { var stringValue: String var intValue: Int? init?(stringValue: String) { self.stringValue = stringValue } init?(intValue: Int) { self.stringValue = String(intValue) self.intValue = intValue } } struct Pair : Decodable { let last : Int let pair : [[Double]] } let jsonString = """ { "last": 123456, "XBTUSD": [ [1.0, 2.0, 3.0], [4.0, 5.0, 6.0] ] } """ do { let decoder = JSONDecoder() decoder.keyDecodingStrategy = .custom({ codingPath in let lastPath = codingPath.last! if lastPath.stringValue == "last" { return lastPath } return AnyKey(stringValue: "pair")! }) let result = try decoder.decode(Pair.self, from: Data(jsonString.utf8)) print(result) } catch { print(error) }¡Ahora tengo un código que realmente funciona!
struct Pair: Decodable { var pair: [[Double]] var last: Int private struct CodingKeys: CodingKey { var intValue: Int? var stringValue: String init?(stringValue: String) { self.stringValue = stringValue } init?(intValue: Int) { self.stringValue = String(intValue) self.intValue = intValue } static let last = CodingKeys(stringValue: "last")! static func makeKey(name: String) -> CodingKeys { return CodingKeys(stringValue: name)! } } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) last = try container.decode(Int.self, forKey: .last) let key = container.allKeys.first(where: { $0.stringValue != "last" } )?.stringValue pair = try container.decode([[Double]].self, forKey: .makeKey(name: key!)) } } init() { let jsonString = """ { "last": 123456, "XBTUSD": [ [1.0, 2.0, 3.0], [4.0, 5.0, 6.0] ] } """ let jsonData = Data(jsonString.utf8) // Ask JSONDecoder to decode the JSON data as DecodedArray let decodedResult = try! JSONDecoder().decode(Pair.self, from: jsonData) dump(decodedResult) }