
El formato JSON ha ganado gran popularidad, generalmente se usa para la transferencia de datos y la ejecución de consultas en aplicaciones cliente-servidor. El análisis JSON requiere herramientas de codificación / decodificación de este formato, y Apple las actualizó recientemente. En este artículo, analizaremos los métodos de análisis JSON utilizando el protocolo
Decodable , compararemos el nuevo protocolo
Codable con el predecesor
NSCoding , evaluaremos las ventajas y desventajas, analizaremos todo con ejemplos específicos y también consideraremos algunas de las características encontradas al implementar los protocolos.
¿Qué es codificable?En WWDC2017, junto con la nueva versión de Swift 4, Apple presentó nuevas herramientas de codificación / decodificación de datos implementadas por los siguientes tres protocolos:
-
Codificable-
Codificable-
DecodificableEn la mayoría de los casos, estos protocolos se usan para trabajar con JSON, pero además también se usan para guardar datos en el disco, transferirlos a través de la red, etc. Encodable se utiliza para convertir estructuras de datos Swift en objetos JSON, mientras que Decodable, por el contrario, ayuda a convertir objetos JSON en modelos de datos Swift. El protocolo codificable combina los dos anteriores y es su tipo:
typealias Codable = Encodable & Decodable
Para cumplir con estos protocolos, los tipos de datos deben implementar los siguientes métodos:
Codificablecodificar (a :) - codifica el modelo de datos en el tipo de codificador dado
Decodificableinit (from :) - inicializa el modelo de datos del decodificador provisto
Codificablecodificar (a :)
init (de :)
Un caso de uso simpleAhora considere un ejemplo simple de uso de
Codificable , ya que implementa
Codificable y
Decodificable , en este ejemplo puede ver de inmediato toda la funcionalidad del protocolo. Digamos que tenemos la estructura de datos JSON más simple:
{ "title": "Nike shoes", "price": 10.5, "quantity": 1 }
El modelo de datos para trabajar con este JSON se verá así:
struct Product: Codable { var title:String var price:Double var quantity:Int enum CodingKeys: String, CodingKey { case title case price case quantity } func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(title, forKey: .title) try container.encode(price, forKey: .price) try container.encode(quantity, forKey: .quantity) } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) title = try container.decode(String.self, forKey: .title) price = try container.decode(Double.self, forKey: .price) quantity = try container.decode(Int.self, forKey: .quantity) } }
Se implementan ambos métodos necesarios, la enumeración también se describe para determinar la lista de campos de codificación / decodificación. De hecho, la escritura puede simplificarse enormemente porque
Codable admite la
generación automática de los métodos codificar (a :) e init (desde :), así como la enumeración necesaria. Es decir, en este caso, puede escribir la estructura de la siguiente manera:
struct Product: Codable { var title:String var price:Double var quantity:Int }
Extremadamente simple y minimalista. Solo, no olvide que una grabación tan concisa no funcionará si:
- la
estructura de su modelo de datos es diferente de la que desea codificar / decodificar-
es posible que deba codificar / decodificar propiedades adicionales además de las propiedades de su modelo de datos-
Algunas propiedades de su modelo de datos pueden no ser compatibles con el protocolo Codificable. En este caso, deberá convertirlos del / al protocolo codificable-
en caso de que los nombres de las variables en el modelo de datos y los nombres de los campos en el contenedor no coincidanComo ya hemos considerado la definición más simple de un modelo de datos, vale la pena dar un pequeño ejemplo de su uso práctico:
Entonces, en una línea, puede analizar la respuesta del servidor en formato JSON:
let product: Product = try! JSONDecoder().decode(Product.self, for: data)
Y el siguiente código, por el contrario, creará un objeto JSON a partir del modelo de datos:
let productObject = Product(title: "Cheese", price: 10.5, quantity: 1) let encodedData = try? JSONEncoder().encode(productObject)
Todo es muy conveniente y rápido.
Una vez descritos correctamente los modelos de datos y convertidos en
codificables , literalmente puede codificar / decodificar datos en una línea. Pero consideramos el modelo de datos más simple que contiene una pequeña cantidad de campos de un tipo simple. Considere los posibles problemas:
No todos los campos en el modelo de datos son codificables.Para que su modelo de datos implemente el protocolo
Codificable, todos los campos del modelo deben ser compatibles con este protocolo. De forma predeterminada, el protocolo
codificable admite los siguientes tipos de datos:
Cadena, Int, Doble, Datos, URL .
Codificable también admite
Array, Diccionario, Opcional , pero solo si contienen tipos
Codificables . Si algunas propiedades del modelo de datos no corresponden a
Codificables , entonces deben ser llevadas a él.
struct Pet: Codable { var name: String var age: Int var type: PetType enum CodingKeys: String, CodingKey { case name case age case type } init(from decoder: Decoder) throws { . . . } func encode(to encoder: Encoder) throws { . . . } }
Si en nuestro
modelo de datos
codificables usamos un tipo personalizado, por ejemplo, como
PetType , y queremos codificarlo / decodificarlo, entonces también debe implementar su init y codificar también.
El modelo de datos no coincide con los campos JSONSi se definen 3 campos en su modelo de datos, y 5 campos llegan a usted en el objeto JSON, 2 de los cuales son adicionales a 3, entonces nada cambiará en el análisis, simplemente obtiene sus 3 campos de esos 5. Si sucede lo contrario situación y en el objeto JSON habrá al menos un campo del modelo de datos, se producirá un error en tiempo de ejecución.
Si algunos campos pueden ser opcionales y ausentes periódicamente en un objeto JSON, entonces en este caso es necesario hacerlos opcionales:
class Product: Codable { var id: Int var productTypeId: Int? var art: String var title: String var description: String var price: Double var currencyId: Int? var brandId: Int var brand: Brand? }
Usando estructuras JSON más complejasA menudo, la respuesta del servidor es una matriz de entidades, es decir, solicita, por ejemplo, una lista de tiendas y obtiene una respuesta en el formulario:
{ "items": [ { "id": 1, "title": " ", "link": "https://www.youtube.com/watch?v=Myp6rSeCMUw", "created_at": 1497868174, "previewImage": "http://img.youtube.com/vi/Myp6rSeCMUw/mqdefault.jpg" }, { "id": 2, "title": " 2", "link": "https://www.youtube.com/watch?v=wsCEuNJmvd8", "created_at": 1525952040, "previewImage": "http://img.youtube.com/vi/wsCEuNJmvd8/mqdefault.jpg" } ] }
En este caso, puede escribirlo y decodificarlo simplemente como una matriz de entidades
Shop .
struct ShopListResponse: Decodable { enum CodingKeys: String, CodingKey { case items } let items: [Shop] }
En este ejemplo, la función automática
init funcionará, pero si desea escribir la decodificación usted mismo, deberá especificar el tipo decodificado como una matriz:
self.items = try container.decode([Shop].self, forKey: .items)
La estructura
Shop también debe implementar el protocolo
Decodable ,
respectivamente. struct Shop: Decodable { var id: Int? var title: String? var address: String? var shortAddress: String? var createdAt: Date? enum CodingKeys: String, CodingKey { case id case title case address case shortAddress = "short_address" case createdAt = "created_at" } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.id = try? container.decode(Int.self, forKey: .id) self.title = try? container.decode(String.self, forKey: .title) self.address = try? container.decode(String.self, forKey: .address) self.shortAddress = try? container.decode(String.self, forKey: .shortAddress) self.createdAt = try? container.decode(Date.self, forKey: .createdAt) } }
Analizar esta matriz de elementos se verá así:
let parsedResult: ShopListResponse = try? JSONDecoder().decode(ShopListResponse.self, from: data)
Por lo tanto, puede trabajar fácilmente con matrices de modelos de datos y usarlos dentro de otros modelos.
Formato de fechaEn este ejemplo, hay un matiz más, aquí encontramos el uso del tipo
Fecha . Cuando se usa este tipo, puede haber problemas con la codificación de la fecha y, por lo general, este problema es coherente con el back-end. El formato predeterminado es
.deferToDate :
struct MyDate : Encodable { let date: Date } let myDate = MyDate(date: Date()) try! encoder.encode(foo)
myDate se verá así:
{ "date" : 519751611.12542897 }
Si necesitamos usar, por ejemplo, el formato
.iso8601 , entonces podemos cambiar fácilmente el formato usando la propiedad
dateEncodingStrategy :
encoder.dateEncodingStrategy = .iso8601
Ahora la fecha se verá así:
{ "date" : "2017-06-21T15:29:32Z" }
También puede usar un formato de fecha personalizado o incluso escribir su propio decodificador de fecha usando las siguientes opciones de formato:
.formatted (DateFormatter) - su propio formato de decodificador de fecha
.custom ((Date, Encoder) throws -> Void) - crea tu propio formato de decodificación de fecha por completo
Analizando objetos anidadosYa hemos examinado cómo puede usar modelos de datos dentro de otros modelos, pero a veces es necesario analizar los campos JSON incluidos en otros campos sin usar un modelo de datos separado. El problema será más claro si lo consideramos con un ejemplo. Tenemos el siguiente JSON:
{ "id": 349, "art": "M0470500", "title": "- Vichy 50 ", "ratings": { "average_rating": 4.1034, "votes_count": 29 } }
Necesitamos analizar los
campos "promedio" y
"recuento de votos" , esto se puede resolver de dos maneras, ya sea creando un modelo de datos de Calificación con dos campos y
guardando los datos en él, o puede usar
nestedContainer . Ya hemos discutido el primer caso, y el uso del segundo se verá así:
class Product: Decodable { var id: Int var art: String? var title: String? var votesCount: Int var averageRating: Double enum CodingKeys: String, CodingKey { case id case art case title case ratings } enum RatingsCodingKeys: String, CodingKey { case votesCount = "votes_count" case averageRating = "average_rating" } required init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.id = try container.decode(Int.self, forKey: .id) self.art = try? container.decode(String.self, forKey: .art) self.title = try? container.decode(String.self, forKey: .title)
Es decir, este problema se resuelve creando otro contenedor adicional utilizando
nestedContainter y su posterior análisis. Esta opción es conveniente si el número de campos anidados no es tan grande; de lo contrario, es mejor utilizar un modelo de datos adicional.
No coinciden los nombres de campo JSON y las propiedades del modelo de datosSi presta atención a cómo se definen las enumeraciones en nuestros modelos de datos, puede ver que a los elementos de las enumeraciones a veces se les asigna una cadena que cambia el valor predeterminado, por ejemplo:
enum RatingsCodingKeys: String, CodingKey { case votesCount = "votes_count" case averageRating = "average_rating" }
Esto se hace para que los nombres de las variables del modelo y los campos JSON coincidan correctamente. Esto generalmente se requiere para los campos cuyo nombre consta de varias palabras, y en JSON están separados por guiones bajos. En principio, tal redefinición de la enumeración es la más popular y parece simple, pero incluso entonces Apple ideó una solución más elegante. Este problema se puede resolver en una línea usando
keyDecodingStrategy . Esta característica apareció en Swift 4.1
Digamos que tiene un JSON de la forma:
let jsonString = """ [ { "name": "MacBook Pro", "screen_size": 15, "cpu_count": 4 }, { "name": "iMac Pro", "screen_size": 27, "cpu_count": 18 } ] """ let jsonData = Data(jsonString.utf8)
Vamos a crear un modelo de datos para ello:
struct Mac: Codable { var name: String var screenSize: Int var cpuCount: Int }
Las variables en el modelo se registran de acuerdo con el acuerdo, comienzan con una letra minúscula y luego cada palabra comienza con una letra mayúscula (el llamado
camelCase ). Pero en JSON, los campos se escriben con guiones bajos (el llamado
snake_case ). Ahora, para que el análisis tenga éxito, necesitamos definir una enumeración en el modelo de datos en el que estableceremos la correspondencia de los nombres de los campos JSON con los nombres de las variables, o obtendremos un error de tiempo de ejecución. Pero ahora es posible simplemente definir
keyDecodingStrategy let decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase do { let macs = try decoder.decode([Mac].self, from: jsonData) } catch { print(error.localizedDescription) }
Para la función de
codificación , puede usar la transformación inversa:
encoder.keyEncodingStrategy = .convertToSnakeCase
También es posible personalizar
keyDecodingStrategy utilizando el siguiente cierre:
let jsonDecoder = JSONDecoder() jsonDecoder.keyDecodingStrategy = .custom { keys -> CodingKey in let key = keys.last!.stringValue.split(separator: "-").joined() return PersonKey(stringValue: String(key))! }
Esta entrada, por ejemplo, permite el uso del separador "-" para JSON. Un ejemplo del JSON utilizado:
{ "first-Name": "Taylor", "last-Name": "Swift", "age": 28 }
Por lo tanto, a menudo se puede evitar una definición adicional de una enumeración.
Manejo de erroresAl analizar JSON y convertir datos de un formato a otro, los errores son inevitables, así que veamos las opciones para manejar diferentes tipos de errores. Al decodificar, son posibles los siguientes tipos de errores:
- DecodingError.dataCorrupted (DecodingError.Context) : los datos están dañados. Por lo general, significa que los datos que intenta decodificar no coinciden con el formato esperado, por ejemplo, en lugar del JSON esperado, recibió un formato completamente diferente.
- DecodingError.keyNotFound (CodingKey, DecodingError.Context) : no se encontró el campo solicitado. Significa que falta el campo que esperaba recibir
- DecodingError.typeMismatch (Any.Type, DecodingError.Context) : no coincide el tipo. Cuando el tipo de datos en el modelo no coincide con el tipo del campo recibido
- DecodingError.valueNotFound (Any.Type, DecodingError.Context) : valor faltante para un campo específico. El campo que definió en el modelo de datos no se pudo inicializar, probablemente en los datos recibidos este campo es nulo. Este error ocurre solo con campos no opcionales, si el campo no tiene que tener un valor, no olvide hacerlo opcional.
Al codificar datos, es posible un error:
EncodingError.invalidValue (Any.Type, DecodingError.Context) : no se pudo convertir el modelo de datos a un formato específico
Un ejemplo de manejo de errores al analizar JSON:
do { let decoder = JSONDecoder() _ = try decoder.decode(businessReviewResponse.self, from: data) } catch DecodingError.dataCorrupted(let context) { print(DecodingError.dataCorrupted(context)) } catch DecodingError.keyNotFound(let key, let context) { print(DecodingError.keyNotFound(key,context)) } catch DecodingError.typeMismatch(let type, let context) { print(DecodingError.typeMismatch(type,context)) } catch DecodingError.valueNotFound(let value, let context) { print(DecodingError.valueNotFound(value,context)) } catch let error{ print(error) }
El procesamiento de errores es, por supuesto, mejor poner una función separada, pero aquí, para mayor claridad, el análisis de errores se realiza junto con el análisis. Por ejemplo, la salida de error si no hay ningún valor para el campo "producto" se verá así:
Comparación de codificables y NSCodingPor supuesto, el protocolo codificable es un gran paso adelante en la codificación / decodificación de datos, pero el protocolo NSCoding existía antes. Intentemos compararlos y ver qué beneficios tiene Codable:
- Cuando se usa el protocolo NSCoding , el objeto debe ser una subclase de NSObject , lo que automáticamente implica que nuestro modelo de datos debe ser una clase. En Codificable , no hay necesidad de herencia, respectivamente, el modelo de datos puede ser tanto de clase como de estructura y enumeración .
- Si necesita funciones de codificación y decodificación separadas, como, por ejemplo, en el caso de análisis de datos JSON recibidos a través de la API, puede usar solo un protocolo decodificable . Es decir, no hay necesidad de implementar los métodos de codificación o init a veces innecesarios.
- Codable puede generar automáticamente los métodos necesarios de inicio y codificación , así como la enumeración opcional de CodingKeys . Esto, por supuesto, solo funciona si tiene campos simples en la estructura de datos, de lo contrario, se requerirá personalización adicional. En la mayoría de los casos, especialmente para estructuras de datos básicas, puede usar la generación automática, especialmente si redefine keyDecodingStrategy , esto es conveniente y reduce algunos códigos innecesarios.
Los
protocolos codificables, decodificables y
codificables nos permitieron dar un paso más hacia la conveniencia de la conversión de datos, aparecieron nuevas herramientas de análisis más flexibles, se redujo la cantidad de código y se automatizó parte de los procesos de conversión. Los protocolos se implementan de forma nativa en Swift 4 y permiten reducir el uso de bibliotecas de terceros, como
SwiftyJSON , mientras se mantiene la usabilidad. Los protocolos también permiten organizar adecuadamente la estructura del código al separar los modelos de datos y los métodos para trabajar con ellos en módulos separados.