read - string to json swift 4
Cómo decodificar una propiedad con un tipo de diccionario JSON en el protocolo decodificable Swift 4 (11)
Digamos que tengo el tipo de datos del
Customer
que contiene una propiedad de
metadata
que puede contener cualquier diccionario JSON en el objeto del cliente
struct Customer {
let id: String
let email: String
let metadata: [String: Any]
}
{
"object": "customer",
"id": "4yq6txdpfadhbaqnwp3",
"email": "[email protected]",
"metadata": {
"link_id": "linked-id",
"buy_count": 4
}
}
La propiedad de
metadata
puede ser cualquier objeto de mapa JSON arbitrario.
Antes de poder emitir la propiedad desde un JSON deserializado de
NSJSONDeserialization
pero con el nuevo protocolo Swift 4
Decodable
, todavía no puedo pensar en una forma de hacerlo.
¿Alguien sabe cómo lograr esto en Swift 4 con el protocolo Decodable?
Aquí hay más genérico (no solo
[String: Any]
, sino que
[Any]
puede decodificarse) y un enfoque encapsulado (se utiliza una entidad separada para eso) inspirado en la respuesta @loudmouth.
Usarlo se verá así:
extension Customer: Decodable {
public init(from decoder: Decoder) throws {
let selfContainer = try decoder.container(keyedBy: CodingKeys.self)
id = try selfContainer.decode(.id)
email = try selfContainer.decode(.email)
let metadataContainer: JsonContainer = try selfContainer.decode(.metadata)
guard let metadata = metadataContainer.value as? [String: Any] else {
let context = DecodingError.Context(codingPath: [CodingKeys.metadata], debugDescription: "Expected ''[String: Any]'' for ''metadata'' key")
throw DecodingError.typeMismatch([String: Any].self, context)
}
self.metadata = metadata
}
private enum CodingKeys: String, CodingKey {
case id, email, metadata
}
}
JsonContainer
es una entidad auxiliar que utilizamos para envolver datos JSON de decodificación en objetos JSON (matriz o diccionario) sin extender
*DecodingContainer
(por lo que no interferirá con casos raros cuando un objeto JSON no se entiende por
[String: Any]
).
struct JsonContainer {
let value: Any
}
extension JsonContainer: Decodable {
public init(from decoder: Decoder) throws {
if let keyedContainer = try? decoder.container(keyedBy: Key.self) {
var dictionary = [String: Any]()
for key in keyedContainer.allKeys {
if let value = try? keyedContainer.decode(Bool.self, forKey: key) {
// Wrapping numeric and boolean types in `NSNumber` is important, so `as? Int64` or `as? Float` casts will work
dictionary[key.stringValue] = NSNumber(value: value)
} else if let value = try? keyedContainer.decode(Int64.self, forKey: key) {
dictionary[key.stringValue] = NSNumber(value: value)
} else if let value = try? keyedContainer.decode(Double.self, forKey: key) {
dictionary[key.stringValue] = NSNumber(value: value)
} else if let value = try? keyedContainer.decode(String.self, forKey: key) {
dictionary[key.stringValue] = value
} else if (try? keyedContainer.decodeNil(forKey: key)) ?? false {
// NOP
} else if let value = try? keyedContainer.decode(JsonContainer.self, forKey: key) {
dictionary[key.stringValue] = value.value
} else {
throw DecodingError.dataCorruptedError(forKey: key, in: keyedContainer, debugDescription: "Unexpected value for /(key.stringValue) key")
}
}
value = dictionary
} else if var unkeyedContainer = try? decoder.unkeyedContainer() {
var array = [Any]()
while !unkeyedContainer.isAtEnd {
let container = try unkeyedContainer.decode(JsonContainer.self)
array.append(container.value)
}
value = array
} else if let singleValueContainer = try? decoder.singleValueContainer() {
if let value = try? singleValueContainer.decode(Bool.self) {
self.value = NSNumber(value: value)
} else if let value = try? singleValueContainer.decode(Int64.self) {
self.value = NSNumber(value: value)
} else if let value = try? singleValueContainer.decode(Double.self) {
self.value = NSNumber(value: value)
} else if let value = try? singleValueContainer.decode(String.self) {
self.value = value
} else if singleValueContainer.decodeNil() {
value = NSNull()
} else {
throw DecodingError.dataCorruptedError(in: singleValueContainer, debugDescription: "Unexpected value")
}
} else {
let context = DecodingError.Context(codingPath: [], debugDescription: "Invalid data format for JSON")
throw DecodingError.dataCorrupted(context)
}
}
private struct Key: CodingKey {
var stringValue: String
init?(stringValue: String) {
self.stringValue = stringValue
}
var intValue: Int?
init?(intValue: Int) {
self.init(stringValue: "/(intValue)")
self.intValue = intValue
}
}
}
Tenga en cuenta que los tipos numéricos y booleanos están respaldados por
NSNumber
, de lo contrario, algo como esto no funcionará:
if customer.metadata["keyForInt"] as? Int64 { // as it always will be nil
Con algo de inspiración de
esta esencia
que encontré, escribí algunas extensiones para
UnkeyedDecodingContainer
y
KeyedDecodingContainer
.
Puede encontrar un enlace a mi esencia
here
.
Al usar este código, ahora puede decodificar cualquier
Array<Any>
o
Dictionary<String, Any>
con la sintaxis familiar:
let dictionary: [String: Any] = try container.decode([String: Any].self, forKey: key)
o
let array: [Any] = try container.decode([Any].self, forKey: key)
Editar:
hay una advertencia que he encontrado que está decodificando una matriz de diccionarios
[[String: Any]]
La sintaxis requerida es la siguiente.
Es probable que desee arrojar un error en lugar de forzar el lanzamiento:
let items: [[String: Any]] = try container.decode(Array<Any>.self, forKey: .items) as! [[String: Any]]
EDIT 2: si simplemente desea convertir un archivo completo en un diccionario, es mejor quedarse con la API de JSONSerialization ya que no he encontrado una manera de extender JSONDecoder para decodificar directamente un diccionario.
guard let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else {
// appropriate error handling
return
}
Las extensiones
// Inspired by https://gist.github.com/mbuchetics/c9bc6c22033014aa0c550d3b4324411a
struct JSONCodingKeys: CodingKey {
var stringValue: String
init?(stringValue: String) {
self.stringValue = stringValue
}
var intValue: Int?
init?(intValue: Int) {
self.init(stringValue: "/(intValue)")
self.intValue = intValue
}
}
extension KeyedDecodingContainer {
func decode(_ type: Dictionary<String, Any>.Type, forKey key: K) throws -> Dictionary<String, Any> {
let container = try self.nestedContainer(keyedBy: JSONCodingKeys.self, forKey: key)
return try container.decode(type)
}
func decodeIfPresent(_ type: Dictionary<String, Any>.Type, forKey key: K) throws -> Dictionary<String, Any>? {
guard contains(key) else {
return nil
}
guard try decodeNil(forKey: key) == false else {
return nil
}
return try decode(type, forKey: key)
}
func decode(_ type: Array<Any>.Type, forKey key: K) throws -> Array<Any> {
var container = try self.nestedUnkeyedContainer(forKey: key)
return try container.decode(type)
}
func decodeIfPresent(_ type: Array<Any>.Type, forKey key: K) throws -> Array<Any>? {
guard contains(key) else {
return nil
}
guard try decodeNil(forKey: key) == false else {
return nil
}
return try decode(type, forKey: key)
}
func decode(_ type: Dictionary<String, Any>.Type) throws -> Dictionary<String, Any> {
var dictionary = Dictionary<String, Any>()
for key in allKeys {
if let boolValue = try? decode(Bool.self, forKey: key) {
dictionary[key.stringValue] = boolValue
} else if let stringValue = try? decode(String.self, forKey: key) {
dictionary[key.stringValue] = stringValue
} else if let intValue = try? decode(Int.self, forKey: key) {
dictionary[key.stringValue] = intValue
} else if let doubleValue = try? decode(Double.self, forKey: key) {
dictionary[key.stringValue] = doubleValue
} else if let nestedDictionary = try? decode(Dictionary<String, Any>.self, forKey: key) {
dictionary[key.stringValue] = nestedDictionary
} else if let nestedArray = try? decode(Array<Any>.self, forKey: key) {
dictionary[key.stringValue] = nestedArray
}
}
return dictionary
}
}
extension UnkeyedDecodingContainer {
mutating func decode(_ type: Array<Any>.Type) throws -> Array<Any> {
var array: [Any] = []
while isAtEnd == false {
// See if the current value in the JSON array is `null` first and prevent infite recursion with nested arrays.
if try decodeNil() {
continue
} else if let value = try? decode(Bool.self) {
array.append(value)
} else if let value = try? decode(Double.self) {
array.append(value)
} else if let value = try? decode(String.self) {
array.append(value)
} else if let nestedDictionary = try? decode(Dictionary<String, Any>.self) {
array.append(nestedDictionary)
} else if let nestedArray = try? decode(Array<Any>.self) {
array.append(nestedArray)
}
}
return array
}
mutating func decode(_ type: Dictionary<String, Any>.Type) throws -> Dictionary<String, Any> {
let nestedContainer = try self.nestedContainer(keyedBy: JSONCodingKeys.self)
return try nestedContainer.decode(type)
}
}
Cuando encontré la respuesta anterior, solo probé un caso de objeto JSON simple pero no vacío, lo que causará una excepción de tiempo de ejecución como @slurmomatic y @zoul encontrado. Perdón por este problema.
Así que intento de otra manera al tener un protocolo JSONValue simple, implementar la estructura de borrado de tipo
AnyJSONValue
y usar ese tipo en lugar de
Any
.
Aquí hay una implementación.
public protocol JSONType: Decodable {
var jsonValue: Any { get }
}
extension Int: JSONType {
public var jsonValue: Any { return self }
}
extension String: JSONType {
public var jsonValue: Any { return self }
}
extension Double: JSONType {
public var jsonValue: Any { return self }
}
extension Bool: JSONType {
public var jsonValue: Any { return self }
}
public struct AnyJSONType: JSONType {
public let jsonValue: Any
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let intValue = try? container.decode(Int.self) {
jsonValue = intValue
} else if let stringValue = try? container.decode(String.self) {
jsonValue = stringValue
} else if let boolValue = try? container.decode(Bool.self) {
jsonValue = boolValue
} else if let doubleValue = try? container.decode(Double.self) {
jsonValue = doubleValue
} else if let doubleValue = try? container.decode(Array<AnyJSONType>.self) {
jsonValue = doubleValue
} else if let doubleValue = try? container.decode(Dictionary<String, AnyJSONType>.self) {
jsonValue = doubleValue
} else {
throw DecodingError.typeMismatch(JSONType.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Unsupported JSON tyep"))
}
}
}
Y aquí está cómo usarlo al decodificar
metadata = try container.decode ([String: AnyJSONValue].self, forKey: .metadata)
El problema con este problema es que debemos llamar a
value.jsonValue as? Int
value.jsonValue as? Int
.
Necesitamos esperar hasta que
Conditional Conformance
aterrice en Swift, eso resolvería este problema o al menos lo ayudaría a mejorar.
[Respuesta anterior]
Publico esta pregunta en el foro de desarrolladores de Apple y resulta que es muy fácil.
puedo hacer
metadata = try container.decode ([String: Any].self, forKey: .metadata)
en el inicializador
En primer lugar, fue mi mal echar de menos eso.
He creado un pod para facilitar la forma de decodificar + codificar
[String: Any]
,
[Any]
.
Y esto proporciona codificar o decodificar las propiedades opcionales, aquí
https://github.com/levantAJ/AnyCodable
pod ''DynamicCodable'', ''1.0''
Cómo usarlo:
import DynamicCodable
struct YourObject: Codable {
var dict: [String: Any]
var array: [Any]
var optionalDict: [String: Any]?
var optionalArray: [Any]?
enum CodingKeys: String, CodingKey {
case dict
case array
case optionalDict
case optionalArray
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
dict = try values.decode([String: Any].self, forKey: .dict)
array = try values.decode([Any].self, forKey: .array)
optionalDict = try values.decodeIfPresent([String: Any].self, forKey: .optionalDict)
optionalArray = try values.decodeIfPresent([Any].self, forKey: .optionalArray)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(dict, forKey: .dict)
try container.encode(array, forKey: .array)
try container.encodeIfPresent(optionalDict, forKey: .optionalDict)
try container.encodeIfPresent(optionalArray, forKey: .optionalArray)
}
}
La forma más fácil y sugerida es crear un modelo separado para cada diccionario o modelo que esté en JSON .
Esto es lo que hago
//Model for dictionary **Metadata**
struct Metadata: Codable {
var link_id: String?
var buy_count: Int?
}
//Model for dictionary **Customer**
struct Customer: Codable {
var object: String?
var id: String?
var email: String?
var metadata: Metadata?
}
//Here is our decodable parser that decodes JSON into expected model
struct CustomerParser {
var customer: Customer?
}
extension CustomerParser: Decodable {
//keys that matches exactly with JSON
enum CustomerKeys: String, CodingKey {
case object = "object"
case id = "id"
case email = "email"
case metadata = "metadata"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CustomerKeys.self) // defining our (keyed) container
let object: String = try container.decode(String.self, forKey: .object) // extracting the data
let id: String = try container.decode(String.self, forKey: .id) // extracting the data
let email: String = try container.decode(String.self, forKey: .email) // extracting the data
//Here I have used metadata model instead of dictionary [String: Any]
let metadata: Metadata = try container.decode(Metadata.self, forKey: .metadata) // extracting the data
self.init(customer: Customer(object: object, id: id, email: email, metadata: metadata))
}
}
Uso:
if let url = Bundle.main.url(forResource: "customer-json-file", withExtension: "json") {
do {
let jsonData: Data = try Data(contentsOf: url)
let parser: CustomerParser = try JSONDecoder().decode(CustomerParser.self, from: jsonData)
print(parser.customer ?? "null")
} catch {
}
}
** He utilizado opcional para estar seguro mientras se analiza, se puede cambiar según sea necesario.
Lo que quieres ir en contra del diseño de
Codable
.
La idea detrás de
Codable
es proporcionar un mecanismo para archivar y desarchivar datos de forma segura.
Esto significa que debe definir las propiedades y sus tipos de datos de antemano.
Puedo pensar en 2 soluciones a su problema:
1. Enumere todas las claves de metadatos potenciales
A menudo, si llega lo suficientemente profundo a la documentación de la API, encontrará la lista completa de todas las claves de metadatos potenciales.
Defina una estructura de
Metadata
, con estas claves como propiedades opcionales:
struct Customer: Decodable {
struct Metadata: Decodable {
var linkId: String?
var buyCount: Int?
var somethingElse: Int?
private enum CodingKeys: String, CodingKey {
case linkId = "link_id"
case buyCount = "buy_count"
case somethingElse = "something_else"
}
}
var object: String
var id: String
var email: String
var metadata: Metadata
}
let customer = try! JSONDecoder().decode(Customer.self, from: jsonData)
print(customer.metadata)
Puedo ver que los diseñadores de Swift hubieran preferido este enfoque.
2. Combinar decodificación y serialización JSONS
JSONSerialization
ofrece un gran dinamismo en la compensación por la seguridad del tipo
Definitivamente puedes mezclarlo con
Decodable
, cuya filosofía de diseño es todo lo contrario:
struct Customer {
private struct RawCustomer: Decodable {
var object: String
var id: String
var email: String
}
var object: String
var id: String
var email: String
var metadata: [String: AnyObject]
init(jsonData: Data) throws {
let rawCustomer = try JSONDecoder().decode(RawCustomer.self, from: jsonData)
object = rawCustomer.object
id = rawCustomer.id
email = rawCustomer.email
let jsonObject = try JSONSerialization.jsonObject(with: jsonData)
if let dict = jsonObject as? [String: AnyObject],
let metadata = dict["metadata"] as? [String: AnyObject]
{
self.metadata = metadata
} else {
self.metadata = [String: AnyObject]()
}
}
}
let customer = try! Customer(jsonData: jsonData)
print(customer.metadata)
Puede crear una estructura de metadatos que confirme el protocolo
Codable
y usar la clase
Decodable
para crear un objeto como el siguiente.
let json: [String: Any] = [
"object": "customer",
"id": "4yq6txdpfadhbaqnwp3",
"email": "[email protected]",
"metadata": [
"link_id": "linked-id",
"buy_count": 4
]
]
struct Customer: Codable {
let object: String
let id: String
let email: String
let metadata: Metadata
}
struct Metadata: Codable {
let link_id: String
let buy_count: Int
}
let data = try JSONSerialization.data(withJSONObject: json, options: .prettyPrinted)
let decoder = JSONDecoder()
do {
let customer = try decoder.decode(Customer.self, from: data)
print(customer)
} catch {
print(error.localizedDescription)
}
Puede echar un vistazo a BeyovaJSON
import BeyovaJSON
struct Customer: Codable {
let id: String
let email: String
let metadata: JToken
}
//create a customer instance
customer.metadata = ["link_id": "linked-id","buy_count": 4]
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
print(String(bytes: try! encoder.encode(customer), encoding: .utf8)!)
Si usa
SwiftyJSON
para analizar JSON, puede actualizar a
4.1.0
que tiene
Codable
protocolo
Codable
.
Simplemente declare
metadata: JSON
y ya está todo listo.
import SwiftyJSON
struct Customer {
let id: String
let email: String
let metadata: JSON
}
También jugué con este problema y finalmente escribí una biblioteca simple para trabajar con tipos "genéricos JSON" . (Donde "genérico" significa "sin estructura conocida de antemano".) El punto principal representa el JSON genérico con un tipo concreto:
public enum JSON {
case string(String)
case number(Float)
case object([String:JSON])
case array([JSON])
case bool(Bool)
case null
}
Este tipo puede implementar
Codable
y
Equatable
.
Vine con una solución ligeramente diferente.
Supongamos que tenemos algo más que un simple
[String: Any]
para analizar donde Any podría ser una matriz o un diccionario anidado o un diccionario de matrices.
Algo como esto:
var json = """
{
"id": 12345,
"name": "Giuseppe",
"last_name": "Lanza",
"age": 31,
"happy": true,
"rate": 1.5,
"classes": ["maths", "phisics"],
"dogs": [
{
"name": "Gala",
"age": 1
}, {
"name": "Aria",
"age": 3
}
]
}
"""
Bueno, esta es mi solución:
public struct AnyDecodable: Decodable {
public var value: Any
private struct CodingKeys: CodingKey {
var stringValue: String
var intValue: Int?
init?(intValue: Int) {
self.stringValue = "/(intValue)"
self.intValue = intValue
}
init?(stringValue: String) { self.stringValue = stringValue }
}
public init(from decoder: Decoder) throws {
if let container = try? decoder.container(keyedBy: CodingKeys.self) {
var result = [String: Any]()
try container.allKeys.forEach { (key) throws in
result[key.stringValue] = try container.decode(AnyDecodable.self, forKey: key).value
}
value = result
} else if var container = try? decoder.unkeyedContainer() {
var result = [Any]()
while !container.isAtEnd {
result.append(try container.decode(AnyDecodable.self).value)
}
value = result
} else if let container = try? decoder.singleValueContainer() {
if let intVal = try? container.decode(Int.self) {
value = intVal
} else if let doubleVal = try? container.decode(Double.self) {
value = doubleVal
} else if let boolVal = try? container.decode(Bool.self) {
value = boolVal
} else if let stringVal = try? container.decode(String.self) {
value = stringVal
} else {
throw DecodingError.dataCorruptedError(in: container, debugDescription: "the container contains nothing serialisable")
}
} else {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Could not serialise"))
}
}
}
Pruébalo usando
let stud = try! JSONDecoder().decode(AnyDecodable.self, from: jsonData).value as! [String: Any]
print(stud)