kotlinx.serialization: qué es, anotaciones y serialización en JSON

Autor: IT Sectr Publicado: 2026-03-15 Tiempo de lectura: 12 min

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 — serialización en tiempo de compilación: el código se genera al compilar, sin usar reflexión
  • @Serializable — la anotación principal que activa la generación del serializador para una clase
  • Json {} builder — configuración JSON mediante Json { ignoreUnknownKeys = true; prettyPrint = true }
  • Multiplataforma — la biblioteca funciona en JVM, Native, JS y Wasm sin cambiar la API
  • Serializadores personalizados — a través de la interfaz KSerializer para formatos de datos no estándar

Qué es kotlinx.serialization

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.

Cómo funciona la generación de código en tiempo de compilación

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.

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")
}

Uso básico: serialización en JSON

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).

kotlin
// 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.

Serialización polimórfica de sealed class

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.

kotlin
// 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.

Anotaciones de kotlinx.serialization: panorama completo

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ónPropósitoEjemplo
@SerializableActiva la generación del serializador para una clase@Serializable data class User
@SerialNameEstablece un nombre alternativo para el campo en el formato@SerialName(“user_name”) val name: String
@TransientExcluye un campo de la serialización@Transient val cache: MutableMap
@RequiredEl campo es obligatorio en JSON durante la deserialización@Required val id: String
@EncodeDefaultSerializa el campo incluso con su valor predeterminado@EncodeDefault val type: Type = Type.A
@SerializerVincula 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.

@Required como alternativa a campos nullable

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.

Serializadores personalizados: KSerializer y control manual

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.

kotlin
// 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) } }.

Formatos de serialización: JSON, ProtoBuf, CBOR, HOCON

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.

FormatoMóduloTipoEsquemaUso típico
JSONkotlinx-serialization-jsonTextoOpcionalAPI REST, almacenamiento de datos
ProtoBufkotlinx-serialization-protobufBinarioObligatorio (.proto)Microservicios, gRPC
CBORkotlinx-serialization-cborBinarioOpcionalIoT, dispositivos móviles
HOCONkotlinx-serialization-hoconTextoOpcionalArchivos 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.

Elección del formato para el proyecto

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.

Errores típicos al trabajar con kotlinx.serialization

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) } } }.

Error de versionado de la biblioteca

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

¿En qué se diferencia kotlinx.serialization de Gson y Moshi?

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.

¿Admite kotlinx.serialization Kotlin Multiplatform?

, 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.

¿Cómo se manejan los campos null en JSON?

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).

¿Qué hacer si el servidor envía campos en snake_case?

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.

¿Se puede serializar Kotlin Flow o corrutinas?

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

  • kotlinx.serialization — serialización en tiempo de compilación mediante @Serializable, sin reflexión, hasta 5 veces más rápido que Gson
  • @Serializable, @SerialName, @Transient — anotaciones clave para configurar la serialización de campos y clases
  • Json {} builder configura JSON: ignoreUnknownKeys, prettyPrint, coerceInputValues, encodeDefaults
  • Sealed class y serialización polimórfica — soporte fluido de jerarquías de tipos sin código adicional
  • KSerializer — interfaz para serializadores personalizados de tipos no estándar (Date, Bitmap, UUID)
  • Cuatro formatos: JSON, ProtoBuf, CBOR, HOCON — se añaden como módulos, API unificada para todos
  • Multiplataforma — código único para JVM, Native, JS y Wasm; crítico para KMM y módulos compartidos

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.

Discutir el proyecto

Lea también