kotlinx.serialization — una libreria multipiattaforma di JetBrains per convertire oggetti Kotlin in JSON, ProtoBuf, CBOR e altri formati senza usare la riflessione. A differenza di Gson e Moshi, genera il codice del serializzatore in fase di compilazione tramite l'annotazione @Serializable, offrendo alte prestazioni e sicurezza dei tipi. Secondo GitHub Kotlin/kotlinx.serialization, la libreria supporta Kotlin/JVM, Kotlin/Native, Kotlin/JS e Kotlin/Wasm.
Punti chiave
kotlinx.serialization è una libreria di serializzazione integrata per Kotlin, sviluppata da JetBrains come parte dell'ecosistema ufficiale Kotlin. La sua principale differenza rispetto alle soluzioni di terze parti (Gson, Moshi, Jackson) è che non utilizza la riflessione in fase di esecuzione. Invece, il codice del serializzatore viene generato in fase di compilazione utilizzando Kotlin Symbol Processing (KSP) o il plugin del compilatore Kotlin. Ciò offre un aumento delle prestazioni fino a 3–5 volte rispetto a Gson e garantisce la sicurezza dei tipi.
La libreria supporta ufficialmente quattro formati: JSON (tramite il modulo kotlinx-serialization-json), ProtoBuf (kotlinx-serialization-protobuf), CBOR (kotlinx-serialization-cbor) e HOCON (kotlinx-serialization-hocon). I formati vengono aggiunti come dipendenze separate in build.gradle.kts, evitando di includere librerie non necessarie nel progetto. Ogni formato ha il proprio insieme di parametri di configurazione.
Multipiattaforma è una caratteristica fondamentale della libreria. La stessa classe con @Serializable funziona su tutti i target: JVM (Android, Backend), Native (iOS), JS (Web, React) e Wasm (WebAssembly). Lo sviluppatore non deve scrivere implementazioni di serializzazione diverse per ogni piattaforma — il codice rimane lo stesso. Ciò è particolarmente prezioso nei progetti Kotlin Multiplatform Mobile (KMM) dove il codice condiviso viene utilizzato su Android e iOS.
La generazione di codice in kotlinx.serialization avviene in tre fasi. Nella prima fase, il compilatore Kotlin rileva l'annotazione @Serializable su una classe e la passa al plugin Kotlin Symbol Processing (KSP). Nella seconda fase, KSP genera un oggetto serializzatore che implementa l'interfaccia KSerializer. Nella terza fase, il codice generato viene compilato insieme al codice sorgente del progetto. Di conseguenza, nessuna di queste fasi viene eseguita durante l'esecuzione dell'applicazione.
Il serializzatore generato lavora direttamente con i campi della classe tramite i loro getter e setter, senza riflessione. Ciò significa che anche i campi con il modificatore private vengono serializzati se sono contrassegnati con @Serializable. Le prestazioni di questo approccio sono vicine alla serializzazione manuale: per classi semplici (5–10 campi), il tempo di serializzazione è di 10–50 microsecondi; per grafi di oggetti complessi, fino a 200 microsecondi per 1000 oggetti.
Per aggiungere la libreria a un progetto Android o Kotlin/JVM, è necessario aggiungere il plugin e le dipendenze in build.gradle.kts. Il plugin org.jetbrains.kotlin.plugin.serialization, con versione corrispondente a quella di Kotlin, attiva la generazione del codice. La libreria kotlinx-serialization-json viene aggiunta nella sezione dependencies con una versione indipendente dalla versione di Kotlin.
// build.gradle.kts — aggiunta di kotlinx.serialization
plugins {
val kotlinVersion = "2.1.0"
kotlin("jvm") version kotlinVersion
kotlin("plugin.serialization") version kotlinVersion
}
dependencies {
// Modulo di serializzazione principale
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3")
// Formati aggiuntivi
implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:1.7.3")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-cbor:1.7.3")
}
JSON è il formato più popolare in kotlinx.serialization. Per serializzare un oggetto, è sufficiente annotare la data class con @Serializable e chiamare Json.encodeToString(). Per la deserializzazione, chiamare Json.decodeFromString() specificando il tipo. La libreria gestisce automaticamente i campi null, le liste, gli oggetti annidati e gli enum. Tutti i campi della classe sono obbligatori per impostazione predefinita, salvo diversa indicazione.
La configurazione JSON viene eseguita tramite il Json {} builder. È possibile passare ignoreUnknownKeys = true per saltare i campi sconosciuti durante la deserializzazione, prettyPrint = true per un output formattato, coerceInputValues = true per convertire valori non validi in valori predefiniti. Sono disponibili anche encodeDefaults (serializzare i campi con valori predefiniti) e classDiscriminator (nome del campo per la serializzazione polimorfa).
// Esempio di serializzazione e deserializzazione 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")
)
// Serializzazione 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"]
}
*/
// Deserializzazione da JSON
val decoded = json.decodeFromString<Project>(jsonString)
println(decoded.name) // kotlinx.serialization
}
L'esempio illustra il ciclo di base di serializzazione e deserializzazione. Una data class Project con l'annotazione @Serializable ottiene automaticamente encodeToString e decodeFromString. Il campo isActive ha un valore predefinito true — se questo campo manca in JSON, viene utilizzato il valore predefinito. Se campi sconosciuti arrivano in JSON senza ignoreUnknownKeys = true, viene lanciata una SerializationException.
Le sealed class sono uno dei casi d'uso più potenti di kotlinx.serialization. La libreria supporta la serializzazione polimorfa per le gerarchie di sealed class senza configurazione aggiuntiva: basta annotare la sealed class e tutte le sue sottoclassi con @Serializable. Durante la serializzazione, viene aggiunto un campo “type” (configurabile tramite classDiscriminator), che determina il tipo specifico durante la deserializzazione.
// Serializzazione polimorfa di 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 serializzazione polimorfa delle sealed class è particolarmente utile nei client API dove il server restituisce diversi tipi di risposta. Senza kotlinx.serialization, si dovrebbe scrivere un deserializzatore manuale con when sul campo discriminatore. Con la libreria, si fa con una singola annotazione. classDiscriminator consente di rinominare il campo marcatore (predefinito “type”) in qualsiasi valore previsto dal server.
La libreria fornisce un insieme di annotazioni per ottimizzare la serializzazione. La principale è @Serializable per una classe. Aggiuntive: @SerialName per impostare il nome di un campo in JSON (se diverso dal nome Kotlin), @Transient per escludere un campo dalla serializzazione, @Required per un campo che deve essere presente in JSON, @EncodeDefault per forzare la serializzazione di un campo anche con il suo valore predefinito.
| Annotazione | Scopo | Esempio |
|---|---|---|
| @Serializable | Attiva la generazione del serializzatore per una classe | @Serializable data class User |
| @SerialName | Imposta un nome alternativo per il campo nel formato | @SerialName(“user_name”) val name: String |
| @Transient | Esclude un campo dalla serializzazione | @Transient val cache: MutableMap |
| @Required | Campo obbligatorio in JSON durante la deserializzazione | @Required val id: String |
| @EncodeDefault | Serializza il campo anche con valore predefinito | @EncodeDefault val type: Type = Type.A |
| @Serializer | Collega un serializzatore personalizzato a una classe | @Serializer(forClass = Date::class) |
L'annotazione @SerialName è fondamentale quando si lavora con API dove i nomi dei campi sono in snake_case, mentre lo stile Kotlin è camelCase. Ad esempio, il server invia “user_id”, mentre il codice Kotlin usa userId. @SerialName(“user_id”) risolve il problema senza mapper aggiuntivi. @Transient è utile per campi che non devono essere inviati al server — come valori calcolati temporanei o cache.
Per impostazione predefinita, tutti i campi in kotlinx.serialization sono obbligatori. Se un campo può essere assente in JSON, è necessario renderlo nullable (String?) o impostare un valore predefinito (val name: String = “”). Tuttavia, esistono situazioni in cui un campo è non nullable in Kotlin ma può mancare in JSON a causa del versionamento dell'API. In questo caso, @Required lancia una SerializationException quando il campo manca, mentre un valore predefinito lo riempie senza errori.
KSerializer è l'interfaccia che tutti i serializzatori in kotlinx.serialization implementano. Se la generazione di codice standard non è adatta (ad esempio, per lavorare con Date, Bitmap o un formato binario specifico), è possibile scrivere il proprio serializzatore. Per farlo, implementare i metodi serialize() e deserialize(), e fornire un descrittore — una descrizione della struttura per lo schema del formato.
I serializzatori personalizzati vengono collegati in due modi: tramite l'annotazione @Serializable(with = MySerializer::class) per il collegamento a una classe specifica, o globalmente tramite Json { serializersModule = ... } per il collegamento a tutte le istanze di un tipo. Il secondo metodo è preferibile per i tipi incorporati (Date, UUID) per evitare di scrivere un'annotazione su ogni campo.
// Serializzatore personalizzato per 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())
}
}
// Utilizzo di un serializzatore personalizzato
@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))
}
Nell'esempio, DateSerializer converte java.util.Date in una stringa ISO 8601. Senza un serializzatore personalizzato, kotlinx.serialization non può lavorare con Date — è un tipo non incluso nella libreria standard di Kotlin. @Serializable(with = DateSerializer::class) su un campo specifico collega il serializzatore solo per quel campo. Per la registrazione globale di tutti i Date, utilizzare Json { serializersModule = SerializersModule { contextual(DateSerializer) } }.
kotlinx.serialization non si limita a JSON. La libreria supporta quattro formati incorporati, ciascuno con il proprio modulo e configurazione. JSON (kotlinx-serialization-json) è universale, leggibile dall'uomo, adatto per API REST. ProtoBuf (kotlinx-serialization-protobuf) è binario, compatto, con schema obbligatorio, per microservizi ad alto carico. CBOR (kotlinx-serialization-cbor) è un equivalente binario di JSON, conveniente per IoT e dispositivi mobili con larghezza di banda limitata. HOCON (kotlinx-serialization-hocon) è un formato di configurazione compatibile con TypeSafe Config.
| Formato | Modulo | Tipo | Schema | Utilizzo tipico |
|---|---|---|---|---|
| JSON | kotlinx-serialization-json | Testo | Opzionale | API REST, archiviazione dati |
| ProtoBuf | kotlinx-serialization-protobuf | Binario | Obbligatorio (.proto) | Microservizi, gRPC |
| CBOR | kotlinx-serialization-cbor | Binario | Opzionale | IoT, dispositivi mobili |
| HOCON | kotlinx-serialization-hocon | Testo | Opzionale | File di configurazione |
ProtoBuf richiede la definizione di uno schema in file .proto, ma kotlinx-serialization-protobuf genera classi Kotlin direttamente da @Serializable senza .proto. Ciò semplifica lo sviluppo: basta annotare la data class e usare ProtoBuf.encodeToByteArray(). CBOR è particolarmente rilevante per Android quando è necessario trasferire dati binari compatti tramite NFC o BLE. I messaggi CBOR sono in media del 20–30% più piccoli di JSON per lo stesso set di dati.
Per le API REST in un'app mobile, JSON è la scelta migliore — può essere debugato senza strumenti aggiuntivi, è leggibile nei log e compatibile con qualsiasi backend. Se l'app trasferisce grandi volumi di dati tra microservizi (centinaia di megabyte), ProtoBuf offre un vantaggio di velocità fino a 5x grazie alla codifica binaria. Per salvare le impostazioni in file, utilizzare HOCON o JSON. Per dispositivi con limiti di traffico rigorosi (sensori IoT), utilizzare CBOR.
Il primo errore è ignorare le chiavi sconosciute durante la deserializzazione. Se il server aggiunge un nuovo campo e si ha ignoreUnknownKeys = false, l'applicazione si blocca con una SerializationException. Questo flag è disattivato per impostazione predefinita. Soluzione: impostare sempre Json { ignoreUnknownKeys = true } per il codice di produzione, per essere resilienti ai cambiamenti dell'API.
Il secondo errore è serializzare i campi internal o private in una data class. In una data class Kotlin, tutti i campi nel costruttore primario vengono serializzati per impostazione predefinita. Se un campo contiene dati sensibili (password, token), deve essere contrassegnato con @Transient o rimosso dal costruttore primario. @Transient esclude completamente il campo dal JSON, ma all'interno del costruttore può causare un errore — è meglio definire tali campi nel corpo della classe con @Transient.
Il terzo errore è la serializzazione polimorfa senza sealed class. Se si utilizza una open class invece di sealed, kotlinx.serialization richiede la registrazione esplicita di tutte le sottoclassi in serializersModule. A differenza delle sealed class, dove il compilatore conosce tutte le sottoclassi, le open class consentono estensioni arbitrarie — la libreria non può determinare automaticamente tutti i sottotipi. La registrazione avviene tramite Json { serializersModule = SerializersModule { polymorphic(Base::class) { subclass(Derived::class) } } }.
La versione di kotlinx.serialization deve essere compatibile con la versione di Kotlin. JetBrains pubblica una tabella di compatibilità: kotlinx-serialization 1.6.x è compatibile con Kotlin 1.9.x, 1.7.x con Kotlin 2.0.x e 2.1.x. La mancata corrispondenza delle versioni causa errori di compilazione criptici come “Symbol ‘serializer’ is missing”. Controllare sempre la versione più recente su Maven Central o nel repository GitHub del progetto.
Domande frequenti
kotlinx.serialization utilizza la generazione di codice in fase di compilazione tramite KSP, mentre Gson e Moshi usano la riflessione in fase di esecuzione. Ciò offre un vantaggio in termini di prestazioni (3–5x più veloce di Gson) e sicurezza dei tipi. Gson serializza qualsiasi campo senza annotazione, il che può portare a perdite di dati. kotlinx.serialization richiede l'annotazione esplicita @Serializable, che è più sicura. Moshi supporta anche codegen, ma solo per JVM e Android.
Sì, kotlinx.serialization è una libreria multipiattaforma ufficiale di JetBrains. Funziona su Kotlin/JVM (Android, Backend), Kotlin/Native (iOS), Kotlin/JS (Web, React) e Kotlin/Wasm. L'API è unificata su tutte le piattaforme: @Serializable + Json.encodeToString() funziona allo stesso modo ovunque. Per iOS non è richiesta alcuna configurazione aggiuntiva — Kotlin/Native compila il codice serializzato in un binario nativo.
I campi nullable (String?) vengono deserializzati come null se il valore è assente o nullo in JSON. Per i campi non nullable (String) senza valore predefinito, l'assenza del campo in JSON genererà una SerializationException. Se si desidera che i valori null non appaiano in JSON, configurare Json { encodeDefaults = false }. Ciò esclude tutti i campi uguali al valore predefinito (incluso null per i tipi nullable).
Utilizzare @SerialName(“nome_in_snake_case”) su ogni campo il cui nome differisce dal formato Kotlin. In alternativa, per Kotlin 2.0+ è disponibile Json { namingStrategy = JsonNamingStrategy.SnakeCase } per la conversione automatica camelCase ↔ snake_case. Questa impostazione si applica a tutti i campi contemporaneamente. Se è necessaria una personalizzazione parziale, combinare @SerialName con la strategia globale.
No, Flow e le coroutine non sono direttamente serializzabili — rappresentano un'esecuzione asincrona, non dati. Per trasferire dati da un Flow, raccoglierli in una collezione tramite .toList() in una coroutine e serializzare la collezione. Allo stesso modo, non è possibile serializzare Job, Deferred o Continuation. Serializzare solo data class — modelli di dati senza logica comportamentale.
Riepilogo
Svilupperemo un'applicazione mobile chiavi in mano
IT Sectr crea applicazioni iOS e Android per startup e aziende dal 2017. Ti consulteremo e ti proporremo la soluzione migliore.
Leggi anche