JASON — JSON para la vida real en Swift

Desde que estoy en el mundo de las apps, parsear JSON siempre ha sido algo incomodo. Me explico. En las apps, la mayoría de veces transformamos un JSON a un objeto. Este JSON a su vez puede tener objetos dentro del objeto principial, y así sucesivamente. Realmente, parece algo sencillo y fácil, muchas veces no tenemos problemas, pero a veces…. A veces no tenemos el _key_ que buscamos, hemos escrito mal el nombre del campo, hay un fallo en el servidor y ese valor no viene, o incluso la documentación está mal y un valor que no aparece como opcional en realidad es opcional.

Debuggear estos errores no es lo más sencillo del mundo. Supongamos que XCode y su debug funcionan perfectamente (que ya es suponer), debemos ir linea a linea en nuestro guard let de 10 propiedades para ver que campo falla, ver cual es el error y modificarlo. En el mejor de los casos.
Rezamos para que el error no esté en un objeto dentro de un objeto, porque ahí el debug se complica…
En realidad, ¿Cuanto tiempo perdemos? ¿Solo son diez minutos no?

Mientras estamos esos diez minutos viendo el error, perdemos el hilo de lo que haciamos. Mientras programamos, no queremos perder el hilo. Es como destruir el castillo de arena mental que llevamos haciendo durante horas. Y todo por qué, ¿por parsear JSON?

NO

De hecho, ni siquiera deberiamos hacer un guard let de diez lineas, con un else generico que nos dirá que algun campo falta, o vete tu a saber. Por que también puede ser que esperemos un número y ese numero esté entre comillas, no?.

La verdad es que no me gusta cuando escribía esas cosas. Sí, escribía. Desde hace un tiempo hasta aquí he estado tratando de mejorar como parseo JSON. Algo sencillo y fácil de escribir, con errores informativos, etc

Un parseo de JSON sea sencillo pero versatil a la vez.

Lo que escribía yo era algo así:

do {
//tenemos el data…
let dictionary = try JSONSerialization.JSONObject(with: data, options: .mutableContainers) as? [String: Any]
 guard
let id = dictionary[“id”] as? Int,
let title = dictionary[“title”] as? String,
let originalLanguage = dictionary[“originalLanguage”] as? String,
let originalTitle = dictionary[“originalTitle”] as? String
else { throw ParseableError.RequiredFieldsNotFound(“❌ \(dictionary) not has all the required fields. “) }
} catch {
//Haz algo aqui…
}

Estoy seguro de que también os sonará.

Vayamos paso a paso. Primero, tenemos once líneas solo para un objeto de cuatro propiedades. Agradecemos no tener otro objeto dentro. El fallo que éste muestra es que no estan todas las propiedades en el diccionario…
Pero no sabemos que objeto estamos creando en la consola y tampoco tenemos mucha idea de que campo es ni que pasa.

Uno de los pasos ha sido meter operadores propios, utilizo un operador en lugar de un guard let. El operador hace el guard let y me da el resultado, tipado. Gracias a los genericos, una funcion que devuelve T infiere el tipo que necesitamos. Dos pajaros de un tiro.

Un ejemplo:

let int: Int = JSON <<< “int”

Pero resulta que puede que nuestro `”int”` no esté. En ese caso, teneos que controlarlo.

let int: Int = try JSON <<< “int”

Controlado. Hemos metido un `try`, ya que lanzamos un error, ese error por lo menos que sea útil e informativo.
¿Que nos falta? Contexto. Lo unico que necesitamos es contexto.

Con contexto, almacenamos que objeto estamos creando, la propiedad que estamos intentando parsear y el tipo a la que queremos convertir. Diferenciamos errores.
- ¿Que objeto estamos creando?
- Puede que nuestra propiedad no esté.
- Puede que esperemos un Int y venga un String

Con el contexto ganamos un log informativo. Sabremos que falla, donde y por qué.

La libreria que he hecho, nos facilita como crear los objetos, como parsearlos y en caso de error, información relevante. No os digo las ventajas si además usais un logger en la nube. Podreís saber el mínimo fallo que haya ocurrido en todas las instalaciones de vuestras apps en producción.

JASON

Como estamos en España, JSON también es JASON. Pues llamemosla JASON.
La librería no es muy invasiva. Lo único que haremos en nuestros objetos será hacerlos ExpressibleByJSON.
Cuando obtengamos un Data en nuestra capa de red, creamos un objeto JSON, y con el empezamos a jugar.

Cualquier objeto que implmente ExpressibleByJSON, se podrá inicializar con un JSON. Requerirá el try, cuyo error será sumamente informativo.
No trabajaremos con opcionales a no ser que sea necesario. No usaremos guard let con casting porque no nos hace falta.

Vamos con un ejemplo mas o menos completo:

struct Message {
let id: Int
let message: String
}
struct Person {
let id: Int
let firstName: String
let secondName: String?
let messages: [Message]
let image: URL
}

Ambos se parsearán así:

extension Message: ExpressibleByJSON {
init(_ json: JSON) throws {
self.id = try json <<< “id”
self.message = try json <<< “message”
}
}
extension Person: ExpressibleByJSON {
init(_ json: JSON) throws {
self.id = try json <<< “id”
self.firstName = try json <<< “first_name”
self.secondName = json <<<? “second_name” // Los opcionales no lanzan, dado que pueden estar o no.
self.messages = try json <<< “messages” // Cuando implementamos ExpressibleByJSON en un objeto, es automaticamente parseable con el operador.
self.image = try json <<< “image” //Podemos extender tipos del sistema para parsearlos de un modo sencillo.
}
}

Esta es la librería, cómo parsea objetos y los errores que lanza. Ahí se contemplan la mayoria de casos.
Cuando recibimos un JSON sin keys, es decir, un array en JSON, también podemos parsearlo. Un ejemplo:

struct FetchMessagesResponse {
let messages: [Message]
}
extension FetchMessagesResponse: ExpressibleByJSON {
init(_ json: JSON) throws {
self.messages = try json<<<
}
}

Si queremos una variable desde el JSON, inferir el tipo es tan sencillo como añadir un as:

let messages = try JSON<<< as [Message] 
//o
let name = try JSON <<< “name” as String

Esto lo he publicado en [GitHub](https://github.com/JulianAlonso/JASON) donde podeis echar un ojo al código. Disponible a través de cocoapods.
Os animo a que la probeis, si veis algun fallo, mandar un pull request con un test que lo demuestre. Los test no son dificiles, hay unos cuantos test hechos en la librería.
Si se os ocurre una nueva feature o cualquier idea, toda ayuda es agradecida.