kotlinx.serialization — una biblioteca multiplataforma de JetBrains para convertir objetos Kotlin a JSON, ProtoBuf, CBOR y otros formatos sin usar reflexión. A diferencia de Gson y Moshi, genera código de serializador en tiempo de compilación mediante la anotación @Serializable, ofreciendo alto rendimiento y seguridad de tipos. Según GitHub Kotlin/kotlinx.serialization, la biblioteca admite Kotlin/JVM, Kotlin/Native, Kotlin/JS y Kotlin/Wasm.
Puntos clave
kotlinx.serialization es una biblioteca de serialización integrada para Kotlin, desarrollada por JetBrains como parte del ecosistema oficial de Kotlin. Su principal diferencia con soluciones de terceros (Gson, Moshi, Jackson) es que no utiliza reflexión en tiempo de ejecución. En su lugar, el código del serializador se genera en tiempo de compilación mediante Kotlin Symbol Processing (KSP) o el plugin del compilador de Kotlin. Esto proporciona un aumento de rendimiento de hasta 3–5 veces en comparación con Gson y garantiza la seguridad de tipos.
La biblioteca admite oficialmente cuatro formatos: JSON (a través del módulo kotlinx-serialization-json), ProtoBuf (kotlinx-serialization-protobuf), CBOR (kotlinx-serialization-cbor) y HOCON (kotlinx-serialization-hocon). Los formatos se añaden como dependencias separadas en build.gradle.kts, evitando incluir bibliotecas innecesarias en el proyecto. Cada formato tiene su propio conjunto de parámetros de configuración.
Multiplataforma es una característica clave de la biblioteca. La misma clase con @Serializable funciona en todos los destinos: JVM (Android, Backend), Native (iOS), JS (Web, React) y Wasm (WebAssembly). El desarrollador no necesita escribir diferentes implementaciones de serialización para cada plataforma — el código sigue siendo el mismo. Esto es especialmente valioso en proyectos Kotlin Multiplatform Mobile (KMM) donde el código compartido se usa en Android e iOS.
La generación de código en kotlinx.serialization ocurre en tres etapas. En la primera etapa, el compilador de Kotlin detecta la anotación @Serializable en una clase y la pasa al plugin Kotlin Symbol Processing (KSP). En la segunda etapa, KSP genera un objeto serializador que implementa la interfaz KSerializer. En la tercera etapa, el código generado se compila junto con el código fuente del proyecto. Como resultado, ninguna de estas etapas se ejecuta durante el tiempo de ejecución de la aplicación.
El serializador generado trabaja directamente con los campos de la clase a través de sus getters y setters, sin reflexión. Esto significa que los campos con el modificador private también se serializan si están marcados con @Serializable. El rendimiento de este enfoque es cercano a la serialización manual: para clases simples (5–10 campos), el tiempo de serialización es de 10–50 microsegundos; para grafos de objetos complejos, hasta 200 microsegundos por 1000 objetos.
Para agregar la biblioteca a un proyecto Android o Kotlin/JVM, debe añadir el plugin y las dependencias en build.gradle.kts. El plugin org.jetbrains.kotlin.plugin.serialization, con la versión que coincida con la de Kotlin, activa la generación de código. La biblioteca kotlinx-serialization-json se agrega en la sección dependencies con una versión independiente de la versión de Kotlin.
// build.gradle.kts — agregando kotlinx.serialization
plugins {
val kotlinVersion = "2.1.0"
kotlin("jvm") version kotlinVersion
kotlin("plugin.serialization") version kotlinVersion
}
dependencies {
// Módulo principal de serialización
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3")
// Formatos adicionales
implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:1.7.3")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-cbor:1.7.3")
}
JSON es el formato más popular en kotlinx.serialization. Para serializar un objeto, basta con anotar la data class con @Serializable y llamar a Json.encodeToString(). Para la deserialización, llame a Json.decodeFromString() especificando el tipo. La biblioteca maneja automáticamente campos null, listas, objetos anidados y enums. Todos los campos de la clase son obligatorios por defecto a menos que se indique lo contrario.
La configuración JSON se realiza mediante el Json {} builder. Puede pasar ignoreUnknownKeys = true para omitir campos desconocidos durante la deserialización, prettyPrint = true para una salida formateada, coerceInputValues = true para convertir valores no válidos en valores predeterminados. También están disponibles encodeDefaults (serializar campos con valores predeterminados) y classDiscriminator (nombre del campo para serialización polimórfica).
// Ejemplo de serialización y deserialización JSON
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonConfiguration
@Serializable
data class Project(
val name: String,
val stars: Int,
val isActive: Boolean = true,
val languages: List<String> = emptyList()
)
fun main() {
val project = Project(
name = "kotlinx.serialization",
stars = 7200,
languages = listOf("Kotlin", "Java")
)
// Serialización JSON con prettyPrint
val json = Json { prettyPrint = true }
val jsonString = json.encodeToString(project)
println(jsonString)
/*
{
"name": "kotlinx.serialization",
"stars": 7200,
"isActive": true,
"languages": ["Kotlin", "Java"]
}
*/
// Deserialización desde JSON
val decoded = json.decodeFromString<Project>(jsonString)
println(decoded.name) // kotlinx.serialization
}
El ejemplo muestra el ciclo básico de serialización y deserialización. Una data class Project con la anotación @Serializable obtiene automáticamente encodeToString y decodeFromString. El campo isActive tiene un valor predeterminado true — si este campo falta en JSON, se usa el valor por defecto. Si llegan campos desconocidos en JSON sin ignoreUnknownKeys = true, se lanza una SerializationException.
Las sealed class son uno de los casos de uso más potentes de kotlinx.serialization. La biblioteca admite la serialización polimórfica para jerarquías de sealed class sin configuración adicional: basta con anotar la sealed class y todas sus subclases con @Serializable. Durante la serialización se agrega un campo “type” (configurable mediante classDiscriminator), que determina el tipo concreto durante la deserialización.
// Serialización polimórfica de sealed class
@Serializable
sealed class Response
@Serializable
data class Success(val data: String) : Response()
@Serializable
data class Error(val code: Int, val message: String) : Response()
fun main() {
val json = Json { classDiscriminator = "result_type" }
val responses: List<Response> = listOf(
Success(data = "Data loaded"),
Error(code = 404, message = "Not found")
)
val jsonString = json.encodeToString(responses)
println(jsonString)
/*
[
{"result_type":"Success","data":"Data loaded"},
{"result_type":"Error","code":404,"message":"Not found"}
]
*/
val decoded = json.decodeFromString<List<Response>>(jsonString)
when (val first = decoded[0]) {
is Success -> println("Success: ${first.data}")
is Error -> println("Error: ${first.code}")
}
}
La serialización polimórfica de sealed class es especialmente útil en clientes API donde el servidor devuelve diferentes tipos de respuesta. Sin kotlinx.serialization, habría que escribir un deserializador manual con when sobre el campo discriminador. Con la biblioteca, esto se hace con una sola anotación. classDiscriminator permite renombrar el campo marcador (por defecto “type”) a cualquier valor esperado por el servidor.
La biblioteca proporciona un conjunto de anotaciones para ajustar finamente la serialización. La principal es @Serializable para una clase. Adicionales: @SerialName para establecer el nombre de un campo en JSON (si difiere del nombre en Kotlin), @Transient para excluir un campo de la serialización, @Required para un campo que debe estar presente en JSON, @EncodeDefault para forzar la serialización de un campo incluso con su valor predeterminado.
| Anotación | Propósito | Ejemplo |
|---|---|---|
| @Serializable | Activa la generación del serializador para una clase | @Serializable data class User |
| @SerialName | Establece un nombre alternativo para el campo en el formato | @SerialName(“user_name”) val name: String |
| @Transient | Excluye un campo de la serialización | @Transient val cache: MutableMap |
| @Required | El campo es obligatorio en JSON durante la deserialización | @Required val id: String |
| @EncodeDefault | Serializa el campo incluso con su valor predeterminado | @EncodeDefault val type: Type = Type.A |
| @Serializer | Vincula un serializador personalizado a una clase | @Serializer(forClass = Date::class) |
La anotación @SerialName es crítica al trabajar con APIs donde los nombres de los campos están en snake_case, mientras que el estilo de Kotlin es camelCase. Por ejemplo, el servidor envía “user_id”, pero en el código Kotlin se usa userId. @SerialName(“user_id”) resuelve este problema sin mapeadores adicionales. @Transient es útil para campos que no deben enviarse al servidor — por ejemplo, valores calculados temporales o cachés.
Por defecto, todos los campos en kotlinx.serialization son obligatorios. Si un campo puede estar ausente en JSON, debe hacerlo nullable (String?) o establecer un valor predeterminado (val name: String = “”). Sin embargo, hay situaciones en las que un campo no es nullable en Kotlin pero puede faltar en JSON debido al versionado de la API. En este caso, @Required lanza una SerializationException cuando el campo falta, mientras que un valor predeterminado lo completa sin error.
KSerializer es la interfaz que implementan todos los serializadores en kotlinx.serialization. Si la generación de código estándar no es adecuada (por ejemplo, para trabajar con Date, Bitmap o un formato binario específico), puede escribir su propio serializador. Para ello, implemente los métodos serialize() y deserialize(), y proporcione un descriptor — una descripción de la estructura para el esquema del formato.
Los serializadores personalizados se conectan de dos maneras: mediante la anotación @Serializable(with = MySerializer::class) para vincularlos a una clase específica, o globalmente mediante Json { serializersModule = ... } para vincularlos a todas las instancias de un tipo. El segundo método es preferible para tipos integrados (Date, UUID) para evitar escribir una anotación en cada campo.
// Serializador personalizado para java.util.Date
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
object DateSerializer : KSerializer<Date> {
private val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US)
override val descriptor: SerialDescriptor =
PrimitiveSerialDescriptor("Date", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: Date) {
encoder.encodeString(dateFormat.format(value))
}
override fun deserialize(decoder: Decoder): Date {
return dateFormat.parse(decoder.decodeString())
}
}
// Uso de un serializador personalizado
@Serializable
data class Event(
val title: String,
@Serializable(with = DateSerializer::class)
val date: Date
)
fun main() {
val json = Json { prettyPrint = true }
val event = Event("Release", Date())
println(json.encodeToString(event))
}
En el ejemplo, DateSerializer convierte java.util.Date a una cadena ISO 8601. Sin un serializador personalizado, kotlinx.serialization no puede trabajar con Date — es un tipo que no forma parte de la biblioteca estándar de Kotlin. @Serializable(with = DateSerializer::class) en un campo específico vincula el serializador solo para ese campo. Para el registro global de todos los Date, use Json { serializersModule = SerializersModule { contextual(DateSerializer) } }.
kotlinx.serialization no se limita a JSON. La biblioteca admite cuatro formatos integrados, cada uno con su propio módulo y configuración. JSON (kotlinx-serialization-json) es universal, legible por humanos, adecuado para APIs REST. ProtoBuf (kotlinx-serialization-protobuf) es binario, compacto, con esquema obligatorio, para microservicios de alta carga. CBOR (kotlinx-serialization-cbor) es un equivalente binario de JSON, conveniente para IoT y dispositivos móviles con ancho de banda limitado. HOCON (kotlinx-serialization-hocon) es un formato de configuración compatible con TypeSafe Config.
| Formato | Módulo | Tipo | Esquema | Uso típico |
|---|---|---|---|---|
| JSON | kotlinx-serialization-json | Texto | Opcional | API REST, almacenamiento de datos |
| ProtoBuf | kotlinx-serialization-protobuf | Binario | Obligatorio (.proto) | Microservicios, gRPC |
| CBOR | kotlinx-serialization-cbor | Binario | Opcional | IoT, dispositivos móviles |
| HOCON | kotlinx-serialization-hocon | Texto | Opcional | Archivos de configuración |
ProtoBuf requiere definir un esquema en archivos .proto, pero kotlinx-serialization-protobuf genera clases Kotlin directamente desde @Serializable sin .proto. Esto simplifica el desarrollo: basta con anotar la data class y usar ProtoBuf.encodeToByteArray(). CBOR es especialmente relevante para Android cuando se necesitan transferir datos binarios compactos a través de NFC o BLE. Los mensajes CBOR son en promedio un 20–30% más pequeños que JSON para el mismo conjunto de datos.
Para APIs REST en una aplicación móvil, JSON es la mejor opción — se depura sin herramientas adicionales, se lee en los registros y es compatible con cualquier backend. Si la aplicación transfiere grandes volúmenes de datos entre microservicios (cientos de megabytes), ProtoBuf ofrece una ventaja de velocidad de hasta 5x gracias a la codificación binaria. Para almacenar configuraciones en archivos, use HOCON o JSON. Para dispositivos con límites estrictos de tráfico (sensores IoT), use CBOR.
El primer error es ignorar claves desconocidas durante la deserialización. Si el servidor agrega un nuevo campo y usted tiene ignoreUnknownKeys = false, la aplicación fallará con una SerializationException. Esta bandera está desactivada por defecto. Solución: configure siempre Json { ignoreUnknownKeys = true } para el código de producción y sea resistente a los cambios de API.
El segundo error es serializar campos internal o private en una data class. En una data class de Kotlin, todos los campos del constructor primario se serializan por defecto. Si un campo contiene datos sensibles (contraseña, token), debe marcarse con @Transient o sacarse del constructor primario. @Transient excluye el campo del JSON por completo, pero dentro del constructor puede causar un error — es mejor definir dichos campos en el cuerpo de la clase con @Transient.
El tercer error es la serialización polimórfica sin sealed class. Si usa una open class en lugar de sealed, kotlinx.serialization requiere el registro explícito de todas las subclases en serializersModule. A diferencia de las sealed class, donde el compilador conoce todas las subclases, las open class permiten una extensión arbitraria — la biblioteca no puede determinar automáticamente todos los subtipos. El registro se realiza mediante Json { serializersModule = SerializersModule { polymorphic(Base::class) { subclass(Derived::class) } } }.
La versión de kotlinx.serialization debe ser compatible con la versión de Kotlin. JetBrains publica una tabla de compatibilidad: kotlinx-serialization 1.6.x es compatible con Kotlin 1.9.x, 1.7.x con Kotlin 2.0.x y 2.1.x. La falta de coincidencia de versiones causa errores de compilación crípticos como “Symbol ‘serializer’ is missing”. Verifique siempre la versión actual en Maven Central o en el repositorio de GitHub del proyecto.
Preguntas frecuentes
kotlinx.serialization utiliza generación de código en tiempo de compilación mediante KSP, mientras que Gson y Moshi usan reflexión en tiempo de ejecución. Esto proporciona una ventaja de rendimiento (3–5 veces más rápido que Gson) y seguridad de tipos. Gson serializa cualquier campo sin anotación, lo que puede provocar fugas de datos. kotlinx.serialization requiere la anotación explícita @Serializable, lo que es más seguro. Moshi también admite codegen, pero solo para JVM y Android.
Sí, kotlinx.serialization es una biblioteca multiplataforma oficial de JetBrains. Funciona en Kotlin/JVM (Android, Backend), Kotlin/Native (iOS), Kotlin/JS (Web, React) y Kotlin/Wasm. La API es uniforme en todas las plataformas: @Serializable + Json.encodeToString() funciona igual en todas partes. Para iOS no se requiere configuración adicional — Kotlin/Native compila el código serializado en un binario nativo.
Los campos nullable (String?) se deserializan como null si el valor falta o es null en JSON. Para campos no nullable (String) sin valor predeterminado, la ausencia del campo en JSON lanzará una SerializationException. Si desea que los valores null no aparezcan en JSON, configure Json { encodeDefaults = false }. Esto excluye todos los campos iguales a su valor predeterminado (incluyendo null para tipos nullable).
Use @SerialName(“nombre_en_snake_case”) en cada campo cuyo nombre difiera del formato de Kotlin. Alternativamente, para Kotlin 2.0+ está disponible Json { namingStrategy = JsonNamingStrategy.SnakeCase } para la conversión automática camelCase ↔ snake_case. Esta configuración se aplica a todos los campos a la vez. Si se necesita una personalización parcial, combine @SerialName con la estrategia global.
No, Flow y las corrutinas no se pueden serializar directamente — representan una ejecución asíncrona, no datos. Para transferir datos desde un Flow, recójalos en una colección mediante .toList() en una corrutina y serialice la colección. Del mismo modo, no se puede serializar Job, Deferred ni Continuation. Serialice solo data classes — modelos de datos sin lógica de comportamiento.
Resumen
Desarrollaremos una aplicación móvil llave en mano
IT Sectr crea aplicaciones para iOS y Android para startups y empresas desde 2017. Le asesoraremos y le propondremos la mejor solución.
Lea también