Escrito por Reinder de Vries el 20 de enero de 2021 en Desarrollo de aplicaciones, iOS, Swift
Puedes usar Codable
en Swift para codificar y decodificar formatos de datos personalizados, como JSON, a objetos nativos de Swift. Es increíblemente fácil asignar objetos Swift a datos JSON, y viceversa, simplemente adoptando el protocolo Codable
.
Como desarrollador pragmático de iOS, te encontrarás con JSON más pronto que tarde. Todos los servicios web, desde Facebook hasta Foursquare, utilizan el formato JSON para obtener datos dentro y fuera de su aplicación. ¿Cómo puedes codificar y decodificar esos datos JSON en objetos Swift de forma efectiva?
En este tutorial aprenderás a trabajar con objetos JSON en Swift, utilizando el protocolo Codable
. Lo que aprenderás, puede ser fácilmente aplicado a otros formatos de datos también. Nos adentraremos en la codificación y decodificación con JSONEncoder
y JSONDecoder
, y te mostraré cómo mediar entre los datos JSON y Swift.
¿Listo? Vamos.
- Comienza: Codificación y decodificación
- El protocolo Codable explicado
- Trabajando con Codable
- Decodificando JSON a objetos Swift con Codable
- Codificando objetos Swift como JSON con Codable
- Trabajando con Arrays anidados y Codable
- Más lecturas
Comienza: Codificación y decodificación
¿Qué problema resuelve realmente el protocolo Codable
en Swift? Empecemos con un ejemplo.
Imagina que estás construyendo una app de recetas. La aplicación muestra varias recetas en una lista, incluyendo los ingredientes, las instrucciones y la información básica sobre los alimentos.
Obtienes los datos para la aplicación desde un servicio web, y su API basada en la nube. Esta API utiliza el formato de datos JSON. Cada vez que solicitas una receta al servicio web, obtienes datos JSON de vuelta.
Aquí tienes un ejemplo de datos JSON para una receta:
{ "name": "Spaghetti Bolognese", "author": "Reinder's Cooking Corner", "url": "https://cookingcorner.com/spaghetti-bolognese", "yield": 4, "ingredients": , "instructions": "Cook spaghetti, fry beef and garlic, add tomatoes, add love, eat!"}
Echa un vistazo a la estructura de los datos JSON.
Los objetos JSON están envueltos en corchetes {
y }
, y las matrices están envueltas en corchetes . Los nombres de las propiedades son cadenas, encerradas entre comillas
"
. Los valores en JSON pueden ser cadenas, números (sin comillas), matrices u otros objetos. También se pueden anidar datos, es decir, matrices dentro de matrices, objetos dentro de matrices, etc., para crear una jerarquía compleja de datos.
JSON es un formato de datos basado en texto que utilizan muchos servicios web, incluidas las API de Twitter, Facebook, Foursquare, etc. Si estás construyendo aplicaciones que utilizan recursos basados en la web, te encontrarás con JSON.
El formato JSON es superior a XML, una alternativa común, porque es eficiente, fácil de analizar y legible por los humanos. JSON es un formato acordado para servicios web, APIs y aplicaciones. Se utiliza en toda la web, aplicaciones y servicios en línea, porque el formato es simple y flexible.
Y JSON tiene una capacidad excelente: se puede codificar cualquier formato de datos en JSON, y decodificar JSON de nuevo a cualquier formato de datos. Este proceso de codificación y decodificación es lo que hace que JSON sea tan poderoso.
Puedes tomar tus valores Swift String
, Int
, Double
, URL
, Date
, Data
, Array
y Dictionary
, y codificarlos como JSON. Luego los envía al webservice, que decodifica los valores en un formato nativo que entiende. Del mismo modo, el servicio web envía los datos codificados como JSON a tu aplicación, y tú decodificas los datos a tipos nativos como String
, Double
y Array
.
Cuando tu aplicación de recetas recibe el JSON (ver arriba), puede ser decodificado en una estructura Swift, como esta:
struct Recipe { var name: String var author: String var url: URL var yield: Int var ingredients: var instructions: String}
En Swift, el protocolo Codable
se utiliza para pasar de un objeto de datos JSON a una clase o estructura Swift real. Esto se llama decodificación, porque los datos JSON se decodifican en un formato que Swift entiende. También funciona a la inversa: codificando objetos Swift como JSON.
El protocolo Codable
en Swift es en realidad un alias de los protocolos Decodable
y Encodable
. Como a menudo se utiliza la codificación y decodificación juntas, se utiliza el protocolo Codable
para obtener ambos protocolos de una sola vez.
La pieza central del flujo de trabajo de codificación/decodificación es el protocolo Codable
de Swift. Averigüemos cómo funciona, a continuación.
¿No puedes distinguir entre «codificación» y «decodificación»? Piénsalo así: Estamos convirtiendo datos de y a «código», como una máquina Enigma o un cifrado criptográfico secreto. Codificar significa convertir los datos en código; en-codificar, o «en/dentro del código». Decodificación significa convertir el código en datos; de-codificación, o «desde/fuera de código».
Consigue que te contraten como desarrollador de iOS
Aprende a crear aplicaciones para iOS 14 con Swift 5
Inscríbete en mi curso de desarrollo de iOS, y aprende a iniciar tu carrera como desarrollador profesional de iOS.
El protocolo codificable explicado
Usar JSON antes de Swift 4 era un poco complicado. Tenías que serializar el JSON tú mismo con JSONSerialization
, y luego convertir cada propiedad del JSON al tipo correcto de Swift.
let json = try? JSONSerialization.jsonObject(with: data, options: )if let recipe = json as? { if let yield = recipe as? Int { recipeObject.yield = yield }}
El fragmento anterior sólo toma un valor yield
del JSON, y lo convierte en Int
. Es extremadamente verboso, y es difícil responder adecuadamente a los posibles errores y discrepancias de tipo. Aunque funciona, no es lo ideal.
Las bibliotecas como SwiftyJSON hacen que trabajar con JSON sea mucho más fácil, pero todavía hay que mapear los datos JSON a sus respectivos objetos y propiedades Swift.
Con Swift 4, y posteriores, se puede utilizar el protocolo Codable
en su lugar. Su estructura o clase Swift simplemente tiene que adoptar el protocolo Codable
, y obtendrá la codificación y decodificación de JSON de forma gratuita.
El protocolo Codable
es una composición de dos protocolos, Decodable y Encodable. Ambos protocolos son bastante mínimos; sólo definen las funciones init(from: Decoder)
y encode(to: Encoder)
, respectivamente. En otras palabras, estas funciones significan que para que un tipo sea «decodable» o «codificable», tendrán que «decodificar de algo» y «codificar a algo».
La verdadera magia de Codable
ocurre con los protocolos Decoder
y Encoder
. Estos protocolos son adoptados por los componentes que codifican/decodifican varios formatos de datos, como JSON. En Swift, tenemos unos cuantos -codificadores:
-
PropertyListEncoder
yPropertyListDecoder
para las listas de propiedades .plist -
JSONEncoder
yJSONDecoder
para JSON – ¡eso somos nosotros! - NSKeyedArchiver puede trabajar con
Codable
, a través dePropertyListEncoder
,Data
yNSCoding
Las clases JSONDecoder
y JSONEncoder
utilizan los protocolos Decoder
y Encoder
para proporcionar la funcionalidad para decodificar/codificar JSON. Eso también significa que puedes escribir tu propio codificador/decodificador personalizado para Codable
, siempre que adoptes los protocolos Decoder
y Encoder
!
¿Necesitas un repaso de los protocolos en Swift? Lee Protocolos en Swift explicados para ponerte al día.
Trabajando con Codable
Vamos a ver un ejemplo. Vamos a mapear algunos datos JSON a una estructura Swift. Esencialmente estamos decodificando el JSON a un objeto Swift real.
Primero, vamos a crear una estructura llamada User
. Así:
struct User: Codable { var first_name: String var last_name: String var country: String}
La estructura User
tiene tres propiedades simples de tipo String
, y se ajusta al protocolo Codable
.
A continuación, vamos a escribir un poco de JSON. Este es el JSON con el que trabajaremos:
{ "first_name": "John", "last_name": "Doe", "country": "United Kingdom"}
Los datos JSON suelen entrar en tu aplicación como respuesta de una petición de webservice, o a través de un archivo .json
, pero para este ejemplo simplemente ponemos el JSON en un String
. Así:
let jsonString = """{ "first_name": "John", "last_name": "Doe", "country": "United Kingdom"}"""
Nota: El código anterior utiliza las comillas triples """
para crear cadenas de varias líneas.
Lo que vamos a hacer ahora es decodificar el JSON y convertirlo en un objeto User
. Así:
let jsonData = jsonString.data(using: .utf8)!let user = try! JSONDecoder().decode(User.self, from: jsonData)print(user.last_name)// Output: Doe
Esto es lo que ocurre:
- Primero, se convierte
jsonString
en un objetoData
llamando a la funcióndata(using:)
sobre la cadena. Este es un paso intermedio necesario. - A continuación, se crea un objeto
JSONDecoder
e inmediatamente se llama a la funcióndecode(_:from:)
sobre él. Esto convierte eljsonData
en un objeto de tipoUser
, mediante la decodificación del JSON. El tipoUser.self
se proporciona como parámetro. - Por último, se imprime el apellido del usuario con
print(user.last_name)
. Ese valoruser
tieneUser
como tipo, así que es un objeto Swift real.
Fácil, ¿verdad? Esencialmente has «mapeado» el objeto JSON a una estructura Swift, y decodificado el formato JSON a un objeto nativo con el que Swift puede trabajar.
En el código anterior, estamos ignorando cualquier error lanzado por decode(_:from:)
con try!
. En su propio código, tendrá que manejar los errores con un bloque do-try-catch
. Un error que puede ser lanzado, por ejemplo, viene de proporcionar JSON inválido.
El User.self
es un metatítulo, una referencia al tipo User
en sí. Le estamos diciendo al decodificador que queremos decodificar los datos con la estructura User
, proporcionándole una referencia a ese tipo.
Un error común al definir tu tipo Codable
, como la estructura User
, es la falta de coincidencia de claves y propiedades. Si has añadido una propiedad a tu struct, que no está presente en el JSON o tiene un tipo diferente, obtendrás un error al decodificar el JSON. Lo contrario, es decir, una propiedad que no existe en tu struct pero sí en el JSON, no es un problema. Es más fácil depurar esto una propiedad a la vez, es decir, seguir añadiendo propiedades hasta que el JSON ya no decodifique sin errores. También puedes determinar qué propiedades/claves incluir o ignorar con el enum CodingKeys
(ver más abajo).
Decodificación de JSON a objetos Swift con Codable
Volvamos a ver el ejemplo de la sección anterior, y ampliémoslo. Aquí está el JSON con el que estamos trabajando:
let jsonString = """{ "first_name": "John", "last_name": "Doe", "country": "United Kingdom"}"""
Luego lo convertimos en un objeto Data
. Así:
let jsonData = jsonString.data(using: .utf8)!
Este es un paso intermedio necesario. En lugar de representar el JSON como una cadena, ahora almacenamos el JSON como un objeto nativo Data
. La codificación de caracteres que estamos utilizando para esa cadena es UTF8.
Si te fijas bien, verás que el código anterior utiliza force unwrapping para trabajar con el valor de retorno opcional de data(using:)
. Desenvolvamos ese opcional de forma más elegante!
if let jsonData = jsonString.data(using: .utf8) { // Use `jsonData`} else { // Respond to error }
Con el código anterior podemos responder a los errores cuando data(using:)
devuelve nil
. Podríamos, por ejemplo, mostrar un mensaje de error, o dejar que la tarea falle silenciosamente y guardar la información de diagnóstico en un registro.
A continuación, vamos a crear un nuevo objeto JSONDecoder
. Así:
let decoder = JSONDecoder()
Luego usamos ese decodificador para decodificar los datos JSON. Así:
let user = try! decoder.decode(User.self, from: jsonData)
Sin embargo, la función decode(_:from:)
puede lanzar errores. El código anterior se bloquea cuando esto ocurre. De nuevo, queremos responder a cualquier error que pueda ocurrir. Así:
do { let user = try decoder.decode(User.self, from: jsonData) print(user.last_name)} catch { print(error.localizedDescription)}
Y todo el bloque de código tiene ahora el siguiente aspecto. ¿Ves cómo es diferente?
if let jsonData = jsonString.data(using: .utf8){ let decoder = JSONDecoder() do { let user = try decoder.decode(User.self, from: jsonData) print(user.last_name) } catch { print(error.localizedDescription) }}
Nunca silencies los errores. Atrapa el error y responde a él, ya sea con la UI/UX, reintentando la tarea, o registrando, fallando y arreglándolo.
Trabajando con CodingKeys
¿Qué pasa si nuestras propiedades JSON, como first_name
y/o firstName
, son diferentes a las propiedades struct de Swift? Ahí es donde entra CodingKeys
.
Cada clase o struct que se ajuste a Codable
puede declarar una enumeración especial anidada llamada CodingKeys
. Se utiliza para definir las propiedades que deben ser codificadas y decodificadas, incluyendo sus nombres.
Veamos un ejemplo. En la estructura User
de abajo, hemos cambiado los nombres de las propiedades de snake_case a camelCase. Por ejemplo, la clave first_name
se llama ahora firstName
.
struct User:Codable { var firstName: String var lastName: String var country: String enum CodingKeys: String, CodingKey { case firstName = "first_name" case lastName = "last_name" case country }}
Cuando uses la estructura anterior con los ejemplos de las secciones anteriores, verás que puedes usar un objeto User
con los nuevos nombres de propiedades. Así:
print(user.firstName)// Output: Johnprint(user.country)// Output: United Kingdom
La enumeración CodingKeys
mapea sus casos a propiedades, y utiliza los valores de las cadenas para seleccionar los nombres de las propiedades correctas en los datos JSON. El caso en la enumeración es el nombre de la propiedad que quieres usar en la estructura, y su valor es el nombre de la clave en los datos JSON.
Codificación de objetos Swift como JSON con Codable
Hasta ahora nos hemos centrado en decodificar datos JSON a objetos Swift. Qué hay de ir en el otro sentido? Podemos también codificar objetos como JSON? Sí, ¡por qué no!
var user = User()user.firstName = "Bob"user.lastName = "and Alice"user.country = "Cryptoland"let jsonData = try! JSONEncoder().encode(user)let jsonString = String(data: jsonData, encoding: .utf8)!print(jsonString)
Y la salida es:
{"country":"Cryptoland","first_name":"Bob","last_name":"and Alice"}
¿Qué pasa aquí?
- Primero, creas un objeto
User
y asignas algunos valores a sus propiedades - Después, utilizas
encode(_:)
para codificar el objetouser
a un objeto JSONData
- Finalmente, conviertes el objeto
Data
enString
y lo imprimes
Si te fijas bien, verás que la codificación sigue los mismos pasos que la decodificación, pero a la inversa. Pasando del objeto Swift, a través del decodificador, dando como resultado una cadena JSON.
Al igual que antes, ¿podemos ampliar el ejemplo para tratar los errores? Sí. Así:
let encoder = JSONEncoder()encoder.outputFormatting = .prettyPrinteddo { let jsonData = try encoder.encode(user) if let jsonString = String(data: jsonData, encoding: .utf8) { print(jsonString) }} catch { print(error.localizedDescription)}
En el ejemplo anterior, también estamos utilizando la propiedad outputFormatting
del codificador para «imprimir bonito» los datos JSON. Esto añade espacios, tabulaciones y nuevas líneas para que la cadena JSON sea más fácil de leer. Así:
{ "country" : "Cryptoland", "first_name" : "Bob", "last_name" : "and Alice"}
¡Impresionante!
Cabe destacar que los diccionarios en Swift, y en JSON, no tienen un orden de clasificación fijo. Por eso, si ejecutas el código anterior varias veces, verás que el orden de las claves country
, first_name
, etc. varía en el JSON. La mayoría de los servicios web que dan salida a JSON impondrán un orden de clasificación fijo a través de un esquema, lo que definitivamente hace más fácil leer y navegar por el JSON.
Trabajar con Arrays anidados y Codable
Hasta ahora hemos trabajado con objetos JSON simples – sólo una clase con algunas propiedades. Pero, ¿y si los datos con los que trabajas son más complejos?
Por ejemplo, los datos JSON que puedes obtener de un webservice de Twitter o Facebook suelen estar anidados. Es decir, los datos a los que quieres llegar están enterrados en 2-3 niveles de matrices y diccionarios JSON. ¿Y ahora qué?
Veamos primero un ejemplo sencillo. Aquí está la estructura Swift con la que trabajaremos:
struct User: Codable { var first_name: String var last_name: String}
Entonces, aquí están los datos JSON que queremos decodificar:
let jsonString = """"""
Si te fijas bien, verás que los datos que necesitamos están almacenados como un array. El JSON no es un simple objeto, sino que es un array con 3 objetos User
. Sabemos que es una matriz de objetos, por los corchetes (matriz) y los corchetes
{ }
(objeto). Los elementos múltiples siempre están separados por comas.
¿Cómo podemos decodificar estos objetos User
? He aquí cómo:
let jsonData = jsonString.data(using: .utf8)!let users = try! JSONDecoder().decode(.self, from: jsonData)for user in users { print(user.first_name)}
El fragmento de código anterior es extremadamente similar a lo que has visto antes, pero hay una diferencia importante. ¿Ves el tipo que estamos proporcionando a la función decode(_:from:)
? El tipo es (con .self), o «array of User objects». En lugar de trabajar con un objeto
User
, queremos decodificar un montón de ellos, y eso se designa con el tipo de matriz .
A continuación, vamos a discutir cómo se puede trabajar con tipos anidados más complejos. Considera, por ejemplo, que el array de objetos User
está anidado dentro de un diccionario JSON. Así:
let jsonString = """{ "users": }"""
En el fragmento de JSON anterior, el elemento de nivel superior es un diccionario (u «objeto»). Sólo tiene un par clave-valor: una matriz users
. Los datos que queremos están dentro de ese array. ¿Cómo llegamos a él?
Es importante que nuestro enfoque no sea demasiado complicado. Es fácil pensar que vamos a necesitar un análisis JSON avanzado, pero resulta que también podemos anidar structs Swift. Vamos a describir todo el JSON como una estructura, con la estructura User
dentro de ella.
Aquí, mira esto:
struct Response: Codable{ struct User: Codable { var first_name: String var last_name: String } var users: }
Esto es lo que pasa:
- El diccionario JSON de nivel superior corresponde al tipo
Response
. Al igual que antes, este tipo se ajusta aCodable
, para soportar la codificación y decodificación de JSON. - Dentro de esa estructura hemos definido otra estructura llamada
User
. Este es exactamente el mismo tipo que hemos utilizado antes. Esta estructura está «anidada». - La estructura
Response
tiene una propiedad llamadausers
de tipo, o array de objetos User.
Lo bueno es que semánticamente, ambas estructuras de datos JSON y Swift son exactamente iguales. Sólo utilizan una sintaxis diferente. Hemos definido una matriz anidada dentro del diccionario de nivel superior, al igual que la estructura Response
tiene una estructura anidada User
y una propiedad users
dentro de ella.
Hacer que funcione es un pedazo de pastel ahora. Mira esto:
let jsonData = jsonString.data(using: .utf8)!let response = try! JSONDecoder().decode(Response.self, from: jsonData)for user in response.users { print(user.first_name)}
¿Ves cómo estamos usando el tipo Response
para decodificar el JSON, y luego hacer un bucle sobre la propiedad response.users
? Comprueba eso con la estructura Response
y el JSON. Estamos recogiendo el par clave-valor users
en el diccionario de nivel superior, y el mapa de los objetos dentro de los objetos User
. El uso de tipos anidados es un gran enfoque de propósito general para estructuras de datos JSON complejas. Cuando te encuentres con un trozo de JSON que no puedas representar fácilmente en un tipo tangible, como User
o Tweet
, intenta ampliar el tipo a algo como Response
o UserCollection
. En lugar de profundizar, amplíalo e incorpora el JSON completo. Utilice tipos anidados para llegar a los datos que desea. También vale la pena señalar aquí que usted no tiene que añadir el User
struct en el Response
struct – puede ir fuera de ella, también.
Consigue que te contraten como desarrollador iOS
Aprende a crear apps para iOS 14 con Swift 5
Inscríbete en mi curso de desarrollo iOS, y aprende a iniciar tu carrera como desarrollador iOS profesional.
Lectura adicional
¡Y esto es todo! Ahora ya lo sabes:
- Cómo usar
Codable
para crear objetos que puedan ser codificados y decodificados - Cómo usar
JSONDecoder
yJSONEncoder
para codificar y decodificar objetos JSON y sus homólogos Swift - Para qué sirve la codificación y decodificación, y por qué es importante en el desarrollo diario de iOS
- Cómo trabajar con datos JSON más complejos, y cómo utilizar tipos anidados
¿Quieres aprender más? Echa un vistazo a estos recursos:
- Arrays, diccionarios y estructuras
- Trabajar con JSON en Swift con SwiftyJSON
- Introducción a la programación orientada a objetos en Swift
- Cómo: Encontrar Un Elemento En Una Matriz En Swift
- Cómo Usar La Documentación Para Desarrolladores De Apple Por Diversión Y Beneficio
- Cómo Encontrar Cadenas Con Expresiones Regulares En Swift
- Por Qué La arquitectura de las aplicaciones es importante
- Explicación de las cadenas en Swift
- Trabajar con archivos en iOS con Swift
- Almacenamiento de datos con NSCoding y NSKeyedArchiver