Blog
KMP Kotlin May 1, 2026

kotlinx.serialization: campos nullable sin default son requeridos

JsonConvertException con 'Field is required but was missing' al migrar de Moshi a kotlinx.serialization — causa raíz y fix con = null

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:

  1. tryCall capturaba la JsonConvertException y devolvía Either.Left.
  2. El repositorio hacía emit(null) al recibir el error.
  3. El colector en el ViewModel hacía route?.let { ... }, que se saltaba el bloque entero.
  4. loading nunca se ponía a false.

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

ComportamientoMoshikotlinx.serialization
Campo T? ausente en JSONAsigna nullLanza JsonConvertException
Campo T? = null ausente en JSONAsigna nullAsigna null
Campo T? con valor null en JSONAsigna nullAsigna null