¿Por qué Gson agrega nulls en mis variables non-nulls? – Kotlin

gson-kotlin-null

¿Kotlin te prometio variables non-null, tu recibes null y no sabes porqué? Descuida, a continuación te explicaré que sucede y algunas posibles soluciones.

Primero, si no sabes que es una variable null y non-null te invito a revisar mi post anterior:

¿Cómo empezar con Kotlin?

Un poco de contexto

Hace algunas semanas estuve trabajando con Retrofit y Gson, había definido la arquitectura de mi aplicación usando MVVM y tenia todo listo para consumir mi primer web service.

Cree mi modelo de respuesta como un data class y continue definiendo sus parámetros como non-null. Imaginémoslo sencillo, algo como esto:

Cuál fue mi sorpresa cuando trono mi aplicación sin razón aparente. Resulto ser que una de mis variables non-null recibió un valor null. ¿Estas jugando conmigo Kotlin?

Ahí estaba yo, recién iniciando en el lenguaje Kotlin, lleno de ilusiones y esperanzas pero totalmente confundido. ¿Qué esta pasando? ¿Las variables non-null pueden ser null? ¿Acaso Kotlin me mintió?

via GIPHY

 ¿Qué sucedió?

Sí, tus variables non-null pueden ser null, pero no es culpa de Kotlin sino de Gson.

Tomemos en cuenta un json con un key llamado name el cual puede tener 3 diferentes estados:

  1. name llega de manera normal
  2. name no llega, es omitido en el json
  3. name llega con un null

Si estas familiarizado con el desarrollo Android seguramente conoces la biblioteca Gson, con su ayuda podemos convertir objetos json a modelos y viceversa.

Es importante entender esto. En el caso 1 Gson serializa el valor de manera normal en nuestro data class. En el caso 2 y 3 Gson serializa el valor como un null, sin importar que la variable del data class haya sido declarada como non-null.

Esto no aplica de la misma forma para los datos primitivos como Int, Double, Float y Boolean.

En Kotlin no existen datos primitivos sino objetos, sin embargo en este post se trataran los tipos Int, Double, Float y Boolean como datos primitivos con fines explicativos.

Los datos primitivos que sean omitidos o lleguen con un valor null en el json tomaran un valor default tal como lo hacen de manera regular en Java. Es decir:

Int – 0
Double – 0.0
Float – 0.0
Boolean – false

Para comprender como es que una variable definida como non-null (es decir, sin el safe operator ?) puede tener un valor null, primero tienes que entender cómo funciona Gson.

Existen diversas cosas que discutir para que puedas entender con una mayor claridad porque sucede esto.  Lo primero es saber ¿Cómo funcionan los constructores en Kotlin?

Volvamos a nuestro data class ReasonModel.

Podemos ver que tenemos un constructor con dos parámetros de tipo string non-null. Sin embargo este es el único constructor que tenemos en esta clase. ¿A qué me refiero?

A diferencia de Java, Kotlin no cuenta con un constructor vacío por default no-args, es decir, nuestra clase ReasonModel no tiene un constructor  implícito como el siguiente:

Para que Kotlin nos genere este constructor no-args es necesario que definamos en cada uno de los parámetros de nuestra data class un valor por defecto, si lo agregamos a nuestra clase ReasonModel se vería así:

Es así que con este constructor no-args Gson genera un objeto de tu clase. Es cuestión de llamarlo utilizando reflexión y hemos terminado. El código dentro del constructor inicializará los campos a los valores predeterminados correctos. 

La parte difícil se produce cuando se trata de entender cómo puede Gson crear instancias de clases que no tienen un constructor sin argumentos e ignorar completamente el proceso de inicialización.

Lo que sucede es que Gson usa Unsafe. Aunque sea difícil de creer, Unsafe  es inseguro (broma :v). Permite omitir la inicialización del objeto evitando todas las comprobaciones de null que se generan en el constructor habitual.

Gson utiliza Unsafe solo como último recurso para tratar de obtener la clase.

Pero esto no significa que Gson este mal, simplemente debemos entender que Gson es una biblioteca creada para Java y no Kotlin.

Así es como accede a nuestra clase sin necesidad de pasar las comprobaciones de nulos y agrega valores null a nuestras variables non-null.

¿Cómo podemos solucionarlo?

Sin lugar a duda no nos gustaría tener todos nuestros data class con parámetros nullables ya que esto supondría una gran cantidad de safe operators en nuestro código para asegurarnos de que las variables no sean null. ¿Te imaginas haciendo algo como esto siempre que uses un string? Bueno, no te preocupes porque  existe una solución.

Lo ideal seria que nuestros modelos nos devuelvan la menor cantidad de datos nulos. Es por ello que necesitamos default values.

De esta manera siempre que obtengamos un parámetro que no sea un modelo, nos gustará recibir los siguientes valores en lugar del valor null:

Int 0
Double 0.0
Float 0.0
Boolean false
String «»
Models (data class) null

Si definimos los tipos Int, Double, Float y Boolean como non-null entonces Gson les agregará su valor default deseado.

Nos gusta que nuestros modelos sean nullables así que tampoco debemos preocuparnos por esto, basta con definir estos modelos con el safe operator:

El problema principal es con los string ya que Gson no va a serializ un valor default como el que se presenta en la tabla en un string («»). El valor que Gson agregará en un string cuando el caso 2 y 3 se presenten será un null.

Solución

Para poder obtener un valor default en nuestro string debemos hacer uso de la annotation @SerializedName:

Hemos declarado dos variables nuevas en el constructor messageSafe y urlSafe.

Son variables private, así que no podremos acceder a ellas desde afuera de la clase, también son nullables, lo que significa que no tienen problema en recibir un valor nulo.

Por otro lado también tenemos dos variables llamadas message y url. Estas variables están declaradas como non-null. 

La etiqueta @Transient nos sirve para indicar que nuestra variable no puede ser serializada, eso es necesario ya que si no la agregamos, Gson encontraría 2 variables con el mismo nombre y ocurriría un error en tiempo de compilación.

Como paso final debemos sobre escribir la función get de cada variable sin guión bajo y utilizamos el elvis operator para devolver vacío («») en caso de que en la variable safe llegue un null.

Conclusión

Esta es una solución simple pero efectiva.

Otra posible solución es utilizando Moshi, te recomiendo leer esta post para tener un mejor conocimiento del tema:
Data Classes and parsing JSON — A Story about converting Models to Kotlin

Referencias

I’ve trusted you! You promised no null pointer exceptions! – +Babbel

Sobre Hugo Figueroa 2 Artículos
Entusiasta de la buenas practicas de programación, amante del rock/metal y del café.