Al migrar los modelos de red de Moshi a kotlinx.serialization en un módulo KMP, me encontré con un spinner de carga que giraba indefinidamente. La petición salía, llegaba respuesta correcta (confirmado en el network inspector), pero el estado de carga nunca se resolvía.
El error
El síntoma era silencioso. Sin crash, sin mensaje de error en pantalla. Solo un loading que nunca pasaba a false. La causa era esta excepción capturada internamente:
io.ktor.serialization.JsonConvertException: Illegal input: Field 'duration' is required
for type with serial name 'com.gasguru.core.network.model.route.NetworkLocalizedValues',
but it was missing at path: $.routes[0].legs[0].steps[0].localizedValues
Por qué ocurre
En Moshi con @JsonClass(generateAdapter = true), un campo val duration: NetworkDuration? sin valor en el JSON se ignora automáticamente y se asigna null. El tipo nullable es suficiente para que Moshi lo trate como opcional.
En kotlinx.serialization, la regla es diferente: un campo nullable sin valor por defecto sigue siendo requerido. La clave debe estar presente en el JSON aunque su valor sea null. Si la clave está completamente ausente, lanza JsonConvertException.
La distinción es importante: nullable (tipo T?) y optional (tiene = null) son conceptos separados en kotlinx.serialization. Moshi los trata como equivalentes; kotlinx.serialization no.
Por qué el spinner no paraba
El flujo de error quedaba oculto por varios niveles de abstracción:
tryCallcapturaba laJsonConvertExceptiony devolvíaEither.Left.- El repositorio hacía
emit(null)al recibir el error. - El colector en el ViewModel hacía
route?.let { ... }, que se saltaba el bloque entero. loadingnunca se ponía afalse.
Ningún nivel propagaba el error a la UI. Sin excepción visible, sin estado de error — solo un spinner eterno.
El fix
Añadir = null como valor por defecto en todos los campos que pueden estar ausentes del JSON:
// Antes — kotlinx.serialization lo trata como requerido aunque el tipo sea nullable
val duration: NetworkDuration?
// Después — campo verdaderamente opcional
val duration: NetworkDuration? = null
La regla al migrar de Moshi
Al migrar modelos de Moshi a kotlinx.serialization, hay que revisar todos los campos T?. La pregunta no es “¿puede ser null este valor?” sino “¿puede estar ausente esta clave en el JSON?”.
- Si la clave puede no aparecer en el JSON → añadir
= null. - Si la clave siempre aparece pero puede tener valor
null→ el tipo nullable sin default es suficiente.
En la práctica, las APIs suelen omitir campos opcionales en lugar de enviarlos como null, así que la mayoría de los T? migrados desde Moshi necesitarán = null.
Resumen
| Comportamiento | Moshi | kotlinx.serialization |
|---|---|---|
Campo T? ausente en JSON | Asigna null | Lanza JsonConvertException |
Campo T? = null ausente en JSON | Asigna null | Asigna null |
Campo T? con valor null en JSON | Asigna null | Asigna null |