kotlinx.serialization — мултиплатформена библиотека от JetBrains за преобразуване на Kotlin обекти в JSON, ProtoBuf, CBOR и други формати без използване на рефлексия. За разлика от Gson и Moshi, тя генерира код на сериализатора на етапа на компилация чрез анотация @Serializable, което осигурява висока производителност и типова безопасност. Според GitHub Kotlin/kotlinx.serialization, библиотеката поддържа Kotlin/JVM, Kotlin/Native, Kotlin/JS и Kotlin/Wasm.
Основни точки
kotlinx.serialization — е вградена библиотека за сериализация за Kotlin, разработена от JetBrains като част от официалната екосистема на Kotlin. Основната ѝ разлика от решенията на трети страни (Gson, Moshi, Jackson) е, че не използва рефлексия по време на изпълнение. Вместо това кодът на сериализатора се генерира на етапа на компилация с помощта на Kotlin Symbol Processing (KSP) или приставката за компилатор на Kotlin. Това осигурява увеличение на производителността до 3-5 пъти в сравнение с Gson и гарантира типова безопасност.
Библиотеката официално поддържа четири формата: JSON (чрез модула kotlinx-serialization-json), ProtoBuf (kotlinx-serialization-protobuf), CBOR (kotlinx-serialization-cbor) и HOCON (kotlinx-serialization-hocon). Форматите се добавят като отделни зависимости в build.gradle.kts, което позволява да не се влачат ненужни библиотеки в проекта. За всеки формат съществува собствен набор от конфигурационни параметри.
Мултиплатформеност — ключова характеристика на библиотеката. Същият клас с @Serializable работи на всички целеви платформи: JVM (Android, Backend), Native (iOS), JS (Web, React) и Wasm (WebAssembly). Разработчикът не трябва да пише различни реализации на сериализация за всяка платформа — кодът остава единен. Това е особено ценно в проекти Kotlin Multiplatform Mobile (KMM), където споделеният код се разпределя между Android и iOS.
Генериране на код в kotlinx.serialization се случва на три етапа. На първия етап компилаторът на Kotlin открива анотацията @Serializable върху клас и я предава на приставката Kotlin Symbol Processing (KSP). На втория етап KSP генерира обект-сериализатор, който имплементира интерфейса KSerializer. На третия етап генерираният код се компилира заедно с изходния код на проекта. В резултат нито един от тези етапи не се изпълнява по време на работа на приложението.
Генерираният сериализатор работи директно с полетата на класа чрез техните getter и setter, без рефлексия. Това означава, че полета с модификатор private също се сериализират, ако са маркирани с @Serializable. Производителността на този подход е близка до ръчната сериализация: за прости класове (5-10 полета) времето за сериализация е 10-50 микросекунди, за сложни графи от обекти — до 200 микросекунди на 1000 обекта.
За свързване на библиотеката в проект Android или Kotlin/JVM трябва да добавите приставката и зависимостите в build.gradle.kts. Приставката org.jetbrains.kotlin.plugin.serialization във версия, съответстваща на версията на Kotlin, активира генерирането на код. Библиотеката kotlinx-serialization-json се добавя в раздела dependencies с версия, независима от версията на Kotlin.
// build.gradle.kts — свързване на kotlinx.serialization
plugins {
val kotlinVersion = "2.1.0"
kotlin("jvm") version kotlinVersion
kotlin("plugin.serialization") version kotlinVersion
}
dependencies {
// Основен модул за сериализация
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3")
// Допълнителни формати
implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:1.7.3")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-cbor:1.7.3")
}
JSON — най-популярният формат в kotlinx.serialization. За сериализация на обект е достатъчно да поставите анотацията @Serializable върху data class и да извикате Json.encodeToString(). За десериализация — Json.decodeFromString() с посочване на типа. Библиотеката автоматично обработва null полета, списъци, вложени обекти и изброявания. Всички полета на класа са по подразбиране задължителни, освен ако не е посочено друго.
Конфигурацията на JSON се извършва чрез Json {} builder. В конструктора могат да се предадат ignoreUnknownKeys = true за пропускане на неизвестни полета при десериализация, prettyPrint = true за форматиран изход, coerceInputValues = true за преобразуване на некоректни стойности в стойности по подразбиране. Също така са достъпни настройките encodeDefaults (сериализация на полета със стойности по подразбиране) и classDiscriminator (име на полето за полиморфна сериализация).
// Пример за сериализация и десериализация на 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")
)
// Сериализация в JSON с prettyPrint
val json = Json { prettyPrint = true }
val jsonString = json.encodeToString(project)
println(jsonString)
/*
{
"name": "kotlinx.serialization",
"stars": 7200,
"isActive": true,
"languages": ["Kotlin", "Java"]
}
*/
// Десериализация от JSON
val decoded = json.decodeFromString<Project>(jsonString)
println(decoded.name) // kotlinx.serialization
}
Примерът демонстрира основния цикъл на сериализация и десериализация. Data class Project с анотация @Serializable автоматично получава encodeToString и decodeFromString. Полето isActive има стойност по подразбиране true — ако това поле липсва в JSON, се използва стойността по подразбиране. Ако в JSON дойдат неизвестни полета без ignoreUnknownKeys = true, ще бъде хвърлено изключение SerializationException.
Sealed class — един от най-мощните случаи на използване на kotlinx.serialization. Библиотеката поддържа полиморфна сериализация за йерархии от sealed class без допълнителна конфигурация: достатъчно е да маркирате sealed class и всички негови наследници с анотация @Serializable. При сериализация се добавя полето „type” (конфигурируемо чрез classDiscriminator), въз основа на което при десериализация се определя конкретният тип.
// Полиморфна сериализация на 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}")
}
}
Полиморфната сериализация на sealed class е особено полезна в API клиенти, където сървърът връща различни типове отговори. Без kotlinx.serialization бихте трябвало да напишете ръчен десериализатор с when по поле-дискриминатор. С библиотеката това се прави с една анотация. classDiscriminator позволява преименуване на полето-маркер (по подразбиране „type”) на произволна стойност, очаквана от сървъра.
Библиотеката предоставя набор от анотации за фина настройка на сериализацията. Основната е @Serializable за класа. Допълнителни анотации: @SerialName за задаване на име на поле в JSON (ако се различава от името в Kotlin), @Transient за изключване на поле от сериализация, @Required за поле, което трябва да присъства в JSON, @EncodeDefault за принудителна сериализация на поле със стойност по подразбиране.
| Анотация | Предназначение | Пример |
|---|---|---|
| @Serializable | Включва генериране на сериализатор за класа | @Serializable data class User |
| @SerialName | Задава алтернативно име на полето във формата | @SerialName(„user_name”) val name: String |
| @Transient | Изключва полето от сериализация | @Transient val cache: MutableMap |
| @Required | Полето е задължително в JSON при десериализация | @Required val id: String |
| @EncodeDefault | Сериализира полето дори със стойност по подразбиране | @EncodeDefault val type: Type = Type.A |
| @Serializer | Свързва потребителски сериализатор с класа | @Serializer(forClass = Date::class) |
Анотацията @SerialName е критична при работа с API, където имената на полета са в snake_case, а стилът на Kotlin е camelCase. Например сървърът изпраща „user_id”, а в кода на Kotlin се използва userId. @SerialName(„user_id”) решава този проблем без допълнителни мапъри. @Transient е удобна за полета, които не трябва да се изпращат на сървъра — например временни изчислени стойности или кеш.
По подразбиране всички полета в kotlinx.serialization са задължителни. Ако поле може да липсва в JSON, трябва да го направите nullable (String?) или да зададете стойност по подразбиране (val name: String = „”). Съществуват обаче ситуации, когато полето не е nullable в Kotlin, но може да липсва в JSON поради версиониране на API. В този случай @Required хвърля SerializationException при липса на поле, докато стойността по подразбиране попълва default без грешка.
KSerializer — интерфейс, който имплементират всички сериализатори в kotlinx.serialization. Ако стандартното генериране на код не е подходящо (например за работа с Date, Bitmap или специфичен двоичен формат), можете да напишете свой собствен сериализатор. За целта трябва да имплементирате методите serialize() и deserialize(), както и да предоставите дескриптор — описание на структурата за схемата на формата.
Потребителските сериализатори се свързват по два начина: чрез анотацията @Serializable(with = MySerializer::class) за свързване с конкретен клас или глобално чрез Json { serializersModule = ... } за свързване с всички инстанции на типа. Вторият начин е за предпочитане за вградени типове (Date, UUID), за да не се налага да пишете анотация на всяко поле.
// Потребителски сериализатор за 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())
}
}
// Използване на потребителски сериализатор
@Serializable
data class Event(
val title: String,
@Serializable(with = DateSerializer::class)
val date: Date
)
fun main() {
val json = Json { prettyPrint = true }
val event = Event("Издание", Date())
println(json.encodeToString(event))
}
В примера DateSerializer преобразува java.util.Date в низ ISO 8601. Без потребителски сериализатор kotlinx.serialization не може да работи с Date — това е тип, който не е част от стандартната библиотека на Kotlin. @Serializable(with = DateSerializer::class) на конкретно поле свързва сериализатора само за това поле. За глобална регистрация на всички Date използвайте Json { serializersModule = SerializersModule { contextual(DateSerializer) } }.
kotlinx.serialization не се ограничава само до JSON. Библиотеката поддържа четири вградени формата, всеки със собствен модул и конфигурация. JSON (kotlinx-serialization-json) — универсален, четим от човек, подходящ за REST API. ProtoBuf (kotlinx-serialization-protobuf) — двоичен, компактен, със задължителна схема, за високонатоварени микросървиси. CBOR (kotlinx-serialization-cbor) — двоичен аналог на JSON, удобен за IoT и мобилни устройства с ограничен трафик. HOCON (kotlinx-serialization-hocon) — конфигурационен формат, съвместим с TypeSafe Config.
| Формат | Модул | Тип | Схема | Типично приложение |
|---|---|---|---|---|
| JSON | kotlinx-serialization-json | Текстов | По избор | REST API, съхранение на данни |
| ProtoBuf | kotlinx-serialization-protobuf | Двоичен | Задължителна (.proto) | Микросървиси, gRPC |
| CBOR | kotlinx-serialization-cbor | Двоичен | По избор | IoT, мобилни устройства |
| HOCON | kotlinx-serialization-hocon | Текстов | По избор | Конфигурационни файлове |
ProtoBuf изисква дефиниране на схема в .proto файлове, но kotlinx-serialization-protobuf генерира Kotlin класове директно от @Serializable без .proto. Това опростява разработката: достатъчно е да анотирате data class и да използвате ProtoBuf.encodeToByteArray(). CBOR е особено актуален за рамката на Android, когато трябва да предавате компактни двоични данни чрез NFC или BLE. Размерът на CBOR съобщението е средно с 20-30% по-малък от JSON при същия набор от данни.
За REST API в мобилно приложение оптимален е JSON — дебъгва се без допълнителни инструменти, четим е в логовете и съвместим с всеки backend. Ако приложението предава големи обеми данни между микросървиси (стотици мегабайта) — ProtoBuf ще даде увеличение на скоростта до 5 пъти благодарение на двоичното кодиране. За съхранение на настройки във файлове използвайте HOCON или JSON. За устройства с строги ограничения на трафика (IoT сензори) — CBOR.
Първа грешка — игнориране на неизвестни ключове при десериализация. Ако сървърът е добавил ново поле и имате ignoreUnknownKeys = false, приложението ще се срине с SerializationException. По подразбиране този флаг е изключен. Решение: винаги задавайте Json { ignoreUnknownKeys = true } за производствен код, за да сте устойчиви на промени в API.
Втора грешка — сериализация на internal или private полета в data class. В Kotlin data class всички полета в първичния конструктор се сериализират по подразбиране. Ако поле съдържа чувствителни данни (парола, токен), трябва да го маркирате с @Transient или да го извадите от първичния конструктор. @Transient изключва полето напълно от JSON, но в конструктора може да предизвика грешка — по-добре дефинирайте такова поле в тялото на класа с @Transient.
Трета грешка — полиморфна сериализация без sealed class. Ако използвате open class вместо sealed, kotlinx.serialization изисква изрична регистрация на всички наследници в serializersModule. За разлика от sealed class, където компилаторът знае всички наследници, open class позволява произволно разширение — библиотеката не може автоматично да определи всички подтипове. Регистрацията се извършва чрез Json { serializersModule = SerializersModule { polymorphic(Base::class) { subclass(Derived::class) } } }.
Версията на kotlinx.serialization трябва да бъде съвместима с версията на Kotlin. JetBrains публикува таблица за съвместимост: kotlinx-serialization 1.6.x е съвместима с Kotlin 1.9.x, 1.7.x — с Kotlin 2.0.x и 2.1.x. Несъответствието на версиите причинява криптични грешки при компилация като „Symbol ‘serializer’ is missing”. Винаги проверявайте актуалната версия в mavenCentral или в хранилището на проекта в GitHub.
Често задавани въпроси
kotlinx.serialization използва генериране на код по време на компилация чрез KSP, докато Gson и Moshi използват рефлексия по време на изпълнение. Това дава предимство в производителността (3-5 пъти по-бързо от Gson) и типова безопасност. Gson сериализира всяко поле без анотация, което може да доведе до изтичане на данни. kotlinx.serialization изисква изрична анотация @Serializable, което е по-безопасно. Moshi също поддържа codegen, но само за JVM и Android.
Да, kotlinx.serialization е официалната мултиплатформена библиотека на JetBrains. Работи на Kotlin/JVM (Android, Backend), Kotlin/Native (iOS), Kotlin/JS (Web, React) и Kotlin/Wasm. API е единен за всички платформи: @Serializable + Json.encodeToString() работи еднакво навсякъде. За iOS не са необходими допълнителни настройки — Kotlin/Native компилира сериализирания код в роден двоичен файл.
Nullable полета (String?) се десериализират като null, ако стойността в JSON липсва или е посочена като null. За non-nullable полета (String) без стойност по подразбиране липсата на поле в JSON ще предизвика SerializationException. Ако искате null стойности да не влизат в JSON, конфигурирайте Json { encodeDefaults = false }. Това ще изключи от изхода всички полета, равни на default (включително null за nullable).
Използвайте @SerialName(„snake_case_name”) на всяко поле, чието име се различава от формата на Kotlin. Алтернативно, за Kotlin 2.0+ е наличен Json { namingStrategy = JsonNamingStrategy.SnakeCase } — автоматично преобразуване camelCase ↔ snake_case. Тази настройка се прилага към всички полета едновременно. Ако е необходимо частично персонализиране, комбинирайте @SerialName с глобалната стратегия.
Не, Flow и корутините не могат да бъдат директно сериализирани — те представляват асинхронно изпълнение, а не данни. За прехвърляне на данни от Flow трябва да ги съберете в колекция чрез .toList() в корутина и да сериализирате колекцията. По подобен начин, Job, Deferred или Continuation не могат да бъдат сериализирани. Сериализирайте само data class — модели на данни без поведенческа логика.
Обобщение
Ще разработим мобилно приложение под ключ
IT Sectr създава iOS и Android приложения за стартъпи и бизнеси от 2017 г. Ще ви консултираме и ще предложим най-доброто решение.
Прочетете също