kotlinx.serialization — wieloplatformowa biblioteka od JetBrains do konwersji obiektów Kotlin na JSON, ProtoBuf, CBOR i inne formaty bez użycia refleksji. W przeciwieństwie do Gson i Moshi, generuje kod serializatora na etapie kompilacji poprzez adnotację @Serializable, co zapewnia wysoką wydajność i bezpieczeństwo typów. Według GitHub Kotlin/kotlinx.serialization, biblioteka wspiera Kotlin/JVM, Kotlin/Native, Kotlin/JS i Kotlin/Wasm.
Najważniejsze
kotlinx.serialization — to wbudowana biblioteka serializacji dla Kotlin, opracowana przez JetBrains jako część oficjalnego ekosystemu Kotlin. Jej główna różnica od rozwiązań firm trzecich (Gson, Moshi, Jackson) polega na tym, że nie używa refleksji w czasie wykonania. Zamiast tego kod serializatora jest generowany na etapie kompilacji za pomocą Kotlin Symbol Processing (KSP) lub wtyczki kompilatora Kotlin. Zapewnia to wzrost wydajności do 3-5 razy w porównaniu z Gson oraz gwarantuje bezpieczeństwo typów.
Biblioteka oficjalnie obsługuje cztery formaty: JSON (poprzez moduł kotlinx-serialization-json), ProtoBuf (kotlinx-serialization-protobuf), CBOR (kotlinx-serialization-cbor) i HOCON (kotlinx-serialization-hocon). Format są podłączane jako osobne zależności w build.gradle.kts, co pozwala nie ciągnąć zbędnych bibliotek do projektu. Dla każdego formatu istnieje własny zestaw parametrów konfiguracyjnych.
Wieloplatformowość — kluczowa cecha biblioteki. Ta sama klasa z @Serializable działa na wszystkich platformach docelowych: JVM (Android, Backend), Native (iOS), JS (Web, React) i Wasm (WebAssembly). Deweloper nie musi pisać różnych implementacji serializacji dla każdej platformy — kod pozostaje jednolity. Jest to szczególnie cenne w projektach Kotlin Multiplatform Mobile (KMM), gdzie wspólny kod jest dzielony między Android i iOS.
Generacja kodu w kotlinx.serialization odbywa się w trzech etapach. W pierwszym etapie kompilator Kotlin wykrywa adnotację @Serializable na klasie i przekazuje ją do wtyczki Kotlin Symbol Processing (KSP). W drugim etapie KSP generuje obiekt-serializator implementujący interfejs KSerializer. W trzecim etapie wygenerowany kod jest kompilowany razem z kodem źródłowym projektu. W rezultacie żaden z tych etapów nie jest wykonywany podczas działania aplikacji.
Wygenerowany serializator działa bezpośrednio z polami klasy poprzez ich gettery i settery, bez refleksji. Oznacza to, że pola z modyfikatorem private również są serializowane, jeśli są oznaczone @Serializable. Wydajność takiego podejścia jest bliska ręcznej serializacji: dla prostych klas (5-10 pól) czas serializacji wynosi 10-50 mikrosekund, dla złożonych grafów obiektów — do 200 mikrosekund na 1000 obiektów.
Aby podłączyć bibliotekę w projekcie Android lub Kotlin/JVM, należy dodać wtyczkę i zależności w build.gradle.kts. Wtyczka org.jetbrains.kotlin.plugin.serialization w wersji zgodnej z wersją Kotlin aktywuje generację kodu. Biblioteka kotlinx-serialization-json jest dodawana w sekcji dependencies z wersją niezależną od wersji Kotlin.
// build.gradle.kts — podłączenie kotlinx.serialization
plugins {
val kotlinVersion = "2.1.0"
kotlin("jvm") version kotlinVersion
kotlin("plugin.serialization") version kotlinVersion
}
dependencies {
// Główny moduł serializacji
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3")
// Dodatkowe formaty
implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:1.7.3")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-cbor:1.7.3")
}
JSON — najpopularniejszy format w kotlinx.serialization. Do serializacji obiektu wystarczy umieścić na data class adnotację @Serializable i wywołać Json.encodeToString(). Do deserializacji — Json.decodeFromString() z określeniem typu. Biblioteka automatycznie obsługuje pola null, listy, zagnieżdżone obiekty i enums. Wszystkie pola klasy są domyślnie wymagane, chyba że określono inaczej.
Konfiguracja JSON jest wykonywana poprzez Json {} builder. W konstruktorze można przekazać ignoreUnknownKeys = true do pomijania nieznanych pól podczas deserializacji, prettyPrint = true do sformatowanego wyjścia, coerceInputValues = true do konwersji nieprawidłowych wartości na wartości domyślne. Dostępne są również ustawienia encodeDefaults (serializacja pól z wartościami domyślnymi) i classDiscriminator (nazwa pola dla polimorficznej serializacji).
// Przykład serializacji i deserializacji 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")
)
// Serializacja do JSON z prettyPrint
val json = Json { prettyPrint = true }
val jsonString = json.encodeToString(project)
println(jsonString)
/*
{
"name": "kotlinx.serialization",
"stars": 7200,
"isActive": true,
"languages": ["Kotlin", "Java"]
}
*/
// Deserializacja z JSON
val decoded = json.decodeFromString<Project>(jsonString)
println(decoded.name) // kotlinx.serialization
}
Przykład demonstruje podstawowy cykl serializacji i deserializacji. Data class Project z adnotacją @Serializable automatycznie otrzymuje encodeToString i decodeFromString. Pole isActive ma domyślną wartość true — jeśli w JSON to pole nie występuje, używana jest wartość domyślna. Jeśli w JSON pojawią się nieznane pola bez ignoreUnknownKeys = true, zostanie wyrzucony wyjątek SerializationException.
Sealed class — jeden z najpotężniejszych przypadków użycia kotlinx.serialization. Biblioteka obsługuje polimorficzną serializację dla hierarchii sealed class bez dodatkowej konfiguracji: wystarczy oznaczyć sealed class i wszystkie jej dziedzice adnotacją @Serializable. Podczas serializacji dodawane jest pole „type” (konfigurowane przez classDiscriminator), na podstawie którego podczas deserializacji określany jest konkretny typ.
// Polimorficzna serializacja 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}")
}
}
Polimorficzna serializacja sealed class jest szczególnie przydatna w klientach API, gdzie serwer zwraca różne typy odpowiedzi. Bez kotlinx.serialization należałoby napisać ręczny deserializator z when według pola-dyskryminatora. Z biblioteką odbywa się to za pomocą jednej adnotacji. classDiscriminator pozwala zmienić nazwę pola-markeru (domyślnie „type”) na dowolną wartość oczekiwaną przez serwer.
Biblioteka udostępnia zestaw adnotacji do precyzyjnego konfigurowania serializacji. Główna to @Serializable dla klasy. Dodatkowe: @SerialName do określania nazwy pola w JSON (jeśli różni się od nazwy Kotlin), @Transient do wykluczenia pola z serializacji, @Required dla pola, które musi być obecne w JSON, @EncodeDefault do wymuszonej serializacji pola z wartością domyślną.
| Adnotacja | Przeznaczenie | Przykład |
|---|---|---|
| @Serializable | Włącza generację serializatora dla klasy | @Serializable data class User |
| @SerialName | Określa alternatywną nazwę pola w formacie | @SerialName(„user_name”) val name: String |
| @Transient | Wyklucza pole z serializacji | @Transient val cache: MutableMap |
| @Required | Pole wymagane w JSON podczas deserializacji | @Required val id: String |
| @EncodeDefault | Serializuje pole nawet z wartością domyślną | @EncodeDefault val type: Type = Type.A |
| @Serializer | Przypisuje niestandardowy serializator do klasy | @Serializer(forClass = Date::class) |
Adnotacja @SerialName jest krytyczna przy pracy z API, gdzie nazwy pól są w snake_case, a styl Kotlin to camelCase. Na przykład serwer przysyła „user_id”, a w kodzie Kotlin używane jest userId. @SerialName(„user_id”) rozwiązuje ten problem bez dodatkowych mapperów. @Transient jest przydatna dla pól, które nie muszą być wysyłane na serwer — na przykład tymczasowe wartości obliczeniowe lub pamięć podręczna.
Domyślnie wszystkie pola w kotlinx.serialization są wymagane. Jeśli pole może być nieobecne w JSON, należy uczynić je nullable (String?) lub ustawić wartość domyślną (val name: String = „”). Istnieją jednak sytuacje, gdy pole nie jest nullable w Kotlin, ale może go nie być w JSON z powodu wersjonowania API. W tym przypadku @Required wyrzuca SerializationException przy braku pola, a wartość domyślna wypełnia default bez błędu.
KSerializer — interfejs implementowany przez wszystkie serializatory w kotlinx.serialization. Jeśli standardowa generacja kodu nie odpowiada (na przykład do pracy z Date, Bitmap lub specyficznym formatem binarnym), można napisać własny serializator. W tym celu należy zaimplementować metody serialize() i deserialize() oraz dostarczyć descriptor — opis struktury dla schematu formatu.
Niestandardowe serializatory są podłączane na dwa sposoby: poprzez adnotację @Serializable(with = MySerializer::class) do przypisania do konkretnej klasy lub globalnie przez Json { serializersModule = ... } do przypisania do wszystkich instancji typu. Drugi sposób jest preferowany dla wbudowanych typów (Date, UUID), aby nie pisać adnotacji na każdym polu.
// Niestandardowy serializator dla 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())
}
}
// Użycie niestandardowego serializatora
@Serializable
data class Event(
val title: String,
@Serializable(with = DateSerializer::class)
val date: Date
)
fun main() {
val json = Json { prettyPrint = true }
val event = Event("Wydanie", Date())
println(json.encodeToString(event))
}
W przykładzie DateSerializer konwertuje java.util.Date na łańcuch ISO 8601. Bez niestandardowego serializatora kotlinx.serialization nie potrafi pracować z Date — to typ, który nie wchodzi w skład standardowej biblioteki Kotlin. @Serializable(with = DateSerializer::class) na konkretnym polu podłącza serializator tylko dla tego pola. Do globalnej rejestracji wszystkich Date użyj Json { serializersModule = SerializersModule { contextual(DateSerializer) } }.
kotlinx.serialization nie ogranicza się do JSON. Biblioteka obsługuje cztery wbudowane formaty, każdy z własnym modułem i konfiguracją. JSON (kotlinx-serialization-json) — uniwersalny, czytelny dla człowieka, odpowiedni dla REST API. ProtoBuf (kotlinx-serialization-protobuf) — binarny, kompaktowy, z obowiązkowym schematem, dla mocno obciążonych mikrousług. CBOR (kotlinx-serialization-cbor) — binarny odpowiednik JSON, wygodny dla IoT i urządzeń mobilnych z ograniczonym transferem. HOCON (kotlinx-serialization-hocon) — format konfiguracyjny, zgodny z TypeSafe Config.
| Format | Moduł | Typ | Schemat | Typowe zastosowanie |
|---|---|---|---|---|
| JSON | kotlinx-serialization-json | Tekstowy | Opcjonalnie | REST API, przechowywanie danych |
| ProtoBuf | kotlinx-serialization-protobuf | Binarny | Wymagany (.proto) | Mikrousługi, gRPC |
| CBOR | kotlinx-serialization-cbor | Binarny | Opcjonalnie | IoT, urządzenia mobilne |
| HOCON | kotlinx-serialization-hocon | Tekstowy | Opcjonalnie | Pliki konfiguracyjne |
ProtoBuf wymaga określenia schematu w plikach .proto, ale kotlinx-serialization-protobuf generuje klasy Kotlin bezpośrednio z @Serializable bez .proto. Upraszcza to rozwój: wystarczy adnotować data class i użyć ProtoBuf.encodeToByteArray(). CBOR jest szczególnie istotny dla frameworka Android, gdy trzeba przesyłać kompaktowe dane binarne przez NFC lub BLE. Rozmiar komunikatu CBOR jest średnio o 20-30% mniejszy niż JSON przy tym samym zestawie danych.
Dla REST API w aplikacji mobilnej optymalny jest JSON — jest debugowany bez dodatkowych narzędzi, czytelny w logach i zgodny z każdym backendem. Jeśli aplikacja przesyła duże ilości danych między mikrousługami (setki megabajtów) — ProtoBuf zapewni wzrost prędkości do 5 razy dzięki kodowaniu binarnemu. Do przechowywania ustawień w plikach użyj HOCON lub JSON. Dla urządzeń z ostrą limitacją transferu (czujniki IoT) — CBOR.
Pierwszy błąd — ignorowanie nieznanych kluczy podczas deserializacji. Jeśli serwer dodał nowe pole, a masz ignoreUnknownKeys = false, aplikacja padnie z SerializationException. Domyślnie ta flaga jest wyłączona. Rozwiązanie: zawsze ustawiaj Json { ignoreUnknownKeys = true } dla kodu produkcyjnego, aby być odpornym na zmiany API.
Drugi błąd — serializacja pól internal lub private w data class. W Kotlin data class wszystkie pola w primary constructor są domyślnie serializowane. Jeśli pole zawiera wrażliwe dane (hasło, token), należy je oznaczyć @Transient lub przenieść z primary constructor. @Transient wyklucza pole z JSON całkowicie, ale w constructor może spowodować błąd — lepiej umieścić takie pole w ciele klasy z @Transient.
Trzeci błąd — polimorficzna serializacja bez sealed class. Jeśli używasz open class zamiast sealed, kotlinx.serialization wymaga jawnej rejestracji wszystkich dziedziców w serializersModule. W przeciwieństwie do sealed class, gdzie kompilator zna wszystkich dziedziców, open class dopuszcza dowolne rozszerzanie — biblioteka nie może automatycznie określić wszystkich podtypów. Rejestracja odbywa się przez Json { serializersModule = SerializersModule { polymorphic(Base::class) { subclass(Derived::class) } } }.
Wersja kotlinx.serialization musi być zgodna z wersją Kotlin. JetBrains publikuje tabelę zgodności: kotlinx-serialization 1.6.x jest zgodna z Kotlin 1.9.x, 1.7.x — z Kotlin 2.0.x i 2.1.x. Niezgodność wersji powoduje tajemnicze błędy kompilacji „Symbol ‘serializer’ is missing”. Zawsze sprawdzaj aktualną wersję na mavenCentral lub w repozytorium GitHub projektu.
Często zadawane pytania
kotlinx.serialization używa generacji kodu w czasie kompilacji przez KSP, a Gson i Moshi używają refleksji w czasie wykonania. Zapewnia to przewagę w wydajności (3-5 razy szybciej niż Gson) i bezpieczeństwie typów. Gson serializuje dowolne pole bez adnotacji, co może prowadzić do wycieku danych. kotlinx.serialization wymaga jawnej adnotacji @Serializable, co jest bezpieczniejsze. Moshi również obsługuje codegen, ale tylko dla JVM i Android.
Tak, kotlinx.serialization to oficjalna wieloplatformowa biblioteka JetBrains. Działa na Kotlin/JVM (Android, Backend), Kotlin/Native (iOS), Kotlin/JS (Web, React) i Kotlin/Wasm. API jest jednolite dla wszystkich platform: @Serializable + Json.encodeToString() działa wszędzie tak samo. Dla iOS nie są wymagane dodatkowe ustawienia — Kotlin/Native kompiluje zserializowany kod do natywnego pliku binarnego.
Pola nullable (String?) są deserializowane jako null, jeśli w JSON wartość nie występuje lub jest określona jako null. Dla pól non-nullable (String) bez wartości domyślnej brak pola w JSON spowoduje SerializationException. Jeśli chcesz, aby wartości null nie trafiały do JSON, skonfiguruj Json { encodeDefaults = false }. Wykluczy to z wyjścia wszystkie pola równe default (w tym null dla nullable).
Użyj @SerialName(„snake_case_name”) na każdym polu, którego nazwa różni się od formatu Kotlin. Alternatywnie, dla Kotlin 2.0+ dostępny jest Json { namingStrategy = JsonNamingStrategy.SnakeCase } — automatyczna konwersja camelCase ↔ snake_case. To ustawienie stosuje się do wszystkich pól jednocześnie. Jeśli potrzebna jest częściowa personalizacja, łącz @SerialName z globalną strategią.
Nie, Flow i korutyny nie są bezpośrednio serializowalne — reprezentują one asynchroniczne wykonanie, a nie dane. Aby przesłać dane z Flow, należy zebrać je do kolekcji poprzez .toList() w korutynie i zserializować kolekcję. Analogicznie, nie można serializować Job, Deferred ani Continuation. Serializuj tylko data class — modele danych bez logiki behawioralnej.
Podsumowanie
Opracujemy aplikację mobilną pod klucz
IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.
Przeczytaj również