Casi cualquier API que consumas o publiques habla JSON. En Kotlin, la herramienta oficial es kotlinx.serialization: marcas una data class con @Serializable y el compilador genera el código que la convierte. Aquí verás cómo configurarla, qué opciones vas a necesitar con API reales y los errores que más aparecen.
También te puede interesar
Qué hace falta
La forma oficial de leer y escribir JSON en Kotlin es kotlinx.serialization, de JetBrains. Tiene dos piezas: un plugin del compilador, que genera en tiempo de compilación el código que convierte cada clase marcada con @Serializable, y la biblioteca con el formato JSON. Como el código se genera al compilar, no usa reflexión: es rápido y funciona igual en Android y en Kotlin Multiplatform. En un proyecto con Gradle, se activan así en build.gradle.kts:
plugins {
kotlin("jvm") version "2.4.21"
kotlin("plugin.serialization") version "2.4.21"
}
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.12.0")
}El plugin lleva la misma versión que Kotlin (aquí 2.4.21) y la biblioteca va por su cuenta (1.12.0 es la actual). Si compilas a mano con kotlinc, el plugin viene dentro de la distribución del compilador y se activa con -Xplugin=$KOTLIN_HOME/lib/kotlinx-serialization-compiler-plugin.jar, poniendo en el classpath los jar kotlinx-serialization-core-jvm y kotlinx-serialization-json-jvm. Así se compiló cada ejemplo de esta página.
De objeto a JSON y de vuelta
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
@Serializable
data class Libro(val titulo: String, val autor: String, val anio: Int, val etiquetas: List<String>)
fun main() {
val libro = Libro("Pedro Páramo", "Juan Rulfo", 1955, listOf("novela", "México"))
val texto = Json.encodeToString(libro)
println(texto)
val vuelta = Json.decodeFromString<Libro>(texto)
println(vuelta)
println(vuelta == libro)
}salida{"titulo":"Pedro Páramo","autor":"Juan Rulfo","anio":1955,"etiquetas":["novela","México"]}
Libro(titulo=Pedro Páramo, autor=Juan Rulfo, anio=1955, etiquetas=[novela, México])
trueencodeToString convierte el objeto en texto y decodeFromString<Libro> hace lo contrario; el tipo entre <> le dice qué clase construir. Los nombres de las propiedades pasan tal cual al JSON, las listas se vuelven arreglos y, al leer, sale una data class igual a la original.
Con sangría, para leerlo
Por omisión el JSON sale en una sola línea, que es lo que conviene para enviarlo por la red. Para guardarlo en un archivo que va a leer una persona, crea una instancia de Json con prettyPrint. Se configura una vez y se reutiliza:
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
@Serializable
data class Libro(val titulo: String, val autor: String, val anio: Int)
val json = Json { prettyPrint = true }
fun main() {
val estante = listOf(
Libro("Pedro Páramo", "Juan Rulfo", 1955),
Libro("Como agua para chocolate", "Laura Esquivel", 1989),
)
println(json.encodeToString(estante))
}salida[
{
"titulo": "Pedro Páramo",
"autor": "Juan Rulfo",
"anio": 1955
},
{
"titulo": "Como agua para chocolate",
"autor": "Laura Esquivel",
"anio": 1989
}
]Lo que revisa el compilador, y lo que no
El plugin revisa las clases marcadas con @Serializable: si una propiedad es de un tipo que no sabe convertir, como LocalDate, el programa no compila.
import java.time.LocalDate import kotlinx.serialization.Serializable @Serializable data class Factura(val folio: String, val fecha: LocalDate)
kotlincpropiedad.kt:5:50: error: serializer was not found for type 'LocalDate'. To use a context serializer as the fallback, explicitly annotate type or property with '@Contextual'.
data class Factura(val folio: String, val fecha: LocalDate)
^^^^^^^^^La solución es escribir un serializador propio para ese tipo, o guardar la fecha como String en ISO ("2026-11-09") y convertirla al usarla. Lo que el plugin no detecta, al menos con Kotlin 2.4.21, es que se te olvide la anotación en la clase que conviertes. Esto compila sin un solo aviso:
import kotlinx.serialization.json.Json
data class Libro(val titulo: String, val anio: Int)
fun main() {
println(Json.encodeToString(Libro("Rayuela", 1963)))
}salidaException in thread "main" kotlinx.serialization.SerializationException: Serializer for class 'Libro' is not found.
Y falla al ejecutarse. El mensaje sigue en una segunda línea: «Please ensure that class is marked as ‘@Serializable’ and that the serialization compiler plugin is applied». Las dos causas posibles son esas: falta la anotación o el plugin no está activado en el build. Si ves este error en un proyecto con Gradle y la clase sí está anotada, revisa la línea kotlin("plugin.serialization").
Campos que tu clase no tiene
Las API suelen devolver más campos de los que te interesan. kotlinx.serialization es estricto por omisión y lo trata como un error:
import kotlinx.serialization.Serializable
import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.Json
@Serializable
data class Producto(val nombre: String, val precio: Double)
val respuesta = """{"nombre": "Molino manual", "precio": 1299.99, "sku": "MOL-01", "stock": 7}"""
fun main() {
try {
println(Json.decodeFromString<Producto>(respuesta))
} catch (e: SerializationException) {
println("${e::class.simpleName}: ${e.message?.lines()?.first()}")
}
val tolerante = Json { ignoreUnknownKeys = true }
println(tolerante.decodeFromString<Producto>(respuesta))
}salidaJsonDecodingException: Unexpected JSON token at offset 48: Encountered an unknown key 'sku' at path: $ Producto(nombre=Molino manual, precio=1299.99)
ignoreUnknownKeys = true es casi obligatorio para leer API de terceros: si mañana añaden un campo, tu programa sigue funcionando. Para tus propios archivos, el modo estricto ayuda a detectar errores de escritura en los nombres.
Valores por omisión, y la sorpresa al escribir
import kotlinx.serialization.Serializable
import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.Json
@Serializable
data class Ajustes(val tema: String = "claro", val tamanoLetra: Int = 16, val usuario: String)
val completo = Json { encodeDefaults = true }
fun main() {
println(Json.decodeFromString<Ajustes>("""{"usuario": "ana"}"""))
try {
Json.decodeFromString<Ajustes>("""{"tema": "oscuro"}""")
} catch (e: SerializationException) {
println("${e::class.simpleName}: ${e.message?.lines()?.first()}")
}
val ajustes = Ajustes(usuario = "ana")
println(Json.encodeToString(ajustes))
println(completo.encodeToString(ajustes))
}salidaAjustes(tema=claro, tamanoLetra=16, usuario=ana)
MissingFieldException: Field 'usuario' is required for type with serial name 'Ajustes', but it was missing at path: $
{"usuario":"ana"}
{"tema":"claro","tamanoLetra":16,"usuario":"ana"}Al leer, una propiedad con valor por omisión puede faltar en el JSON; una sin él, no (es la MissingFieldException de la segunda línea). La sorpresa está al escribir: por omisión no se escriben las propiedades que tienen su valor por omisión, y el JSON sale solo con el usuario. Es intencional (ahorra bytes), pero si otro programa espera encontrar todos los campos, activa encodeDefaults = true.
Nombres distintos en el JSON
Las API escriben a menudo id_pedido o fecha_envio, y en Kotlin queremos id y fechaEnvio. @SerialName traduce entre los dos. También sirve para los valores de un enum:
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
@Serializable
enum class Estado {
@SerialName("pendiente") PENDIENTE,
@SerialName("enviado") ENVIADO,
}
@Serializable
data class Pedido(
@SerialName("id_pedido") val id: Int,
val estado: Estado,
@SerialName("fecha_envio") val fechaEnvio: String? = null,
val nota: String? = null,
)
fun main() {
val pedido = Pedido(1042, Estado.ENVIADO, "2026-11-09")
println(Json.encodeToString(pedido))
val leido = Json.decodeFromString<Pedido>("""{"id_pedido": 7, "estado": "pendiente", "nota": null}""")
println(leido)
}salida{"id_pedido":1042,"estado":"enviado","fecha_envio":"2026-11-09"}
Pedido(id=7, estado=PENDIENTE, fechaEnvio=null, nota=null)Las propiedades que aceptan null se escriben como null cuando valen eso, salvo que tengan valor por omisión (entonces se omiten, como acabas de ver). Al leer, un null explícito o la ausencia del campo dan lo mismo si el tipo es String? con = null.
Sin clases: el árbol de JSON
Para echar un vistazo a una respuesta, o cuando la forma del JSON no es fija, puedes recorrerlo como árbol con parseToJsonElement, y construir uno con buildJsonObject:
import kotlinx.serialization.json.*
fun main() {
val raiz = Json.parseToJsonElement(
"""{"tienda": "Centro", "ventas": [{"producto": "café", "uds": 3}, {"producto": "té", "uds": 5}]}"""
).jsonObject
println(raiz["tienda"]?.jsonPrimitive?.content)
for (v in raiz["ventas"]!!.jsonArray) {
val o = v.jsonObject
println("${o["producto"]!!.jsonPrimitive.content}: ${o["uds"]!!.jsonPrimitive.int}")
}
val nuevo = buildJsonObject {
put("tienda", "Norte")
put("abierta", true)
putJsonArray("ventas") {
addJsonObject { put("producto", "cacao"); put("uds", 2) }
}
}
println(nuevo)
}salidaCentro
café: 3
té: 5
{"tienda":"Norte","abierta":true,"ventas":[{"producto":"cacao","uds":2}]}jsonPrimitive.content da el valor como texto y jsonPrimitive.int lo convierte en número. Para datos que vas a usar en serio, mejor una data class: con el árbol, cada !! es un sitio donde el programa puede tronar si el campo falta.
Un tipo que no cuadra
import kotlinx.serialization.Serializable
import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.Json
@Serializable
data class Producto(val nombre: String, val precio: Double)
fun main() {
try {
Json.decodeFromString<Producto>("""{"nombre": "Taza", "precio": "barata"}""")
} catch (e: SerializationException) {
println(e.message)
}
}salidaUnexpected JSON token at offset 37: Failed to parse type 'double' for input 'barata' at path: $.precio
JSON input: {"nombre": "Taza", "precio": "barata"}El mensaje (en inglés) dice qué falló y en qué ruta del JSON ($.precio), y muestra el fragmento. Al leer datos de fuera, atrapa SerializationException, que es la madre de todos estos errores.
- ParaUsa
- Marcar una clase
@Serializable(y el plugin activado) - Objeto a texto
Json.encodeToString(x) - Texto a objeto
Json.decodeFromString<T>(texto) - Con sangría
Json { prettyPrint = true } - Ignorar campos de más
Json { ignoreUnknownKeys = true } - Escribir los valores por omisión
Json { encodeDefaults = true } - Otro nombre en el JSON
@SerialName("fecha_envio") - Sin clases
parseToJsonElement,buildJsonObject
Si vienes de Java: Jackson también funciona
Desde Kotlin puedes usar Jackson (con su módulo jackson-module-kotlin) o Gson, como en Java; mira JSON con Jackson en Java. La ventaja de kotlinx.serialization es que entiende los tipos de Kotlin sin trucos: los null, los valores por omisión y las data class, y que no usa reflexión.
Todo el código de esta página se compiló con Kotlin 2.4.21 y se ejecutó en la JVM de Temurin 25.0.5 en un contenedor limpio y sin red antes de publicarla; las salidas y los mensajes del compilador están copiados de esa ejecución.