kotlinx.serialization: какво е това, анотации и сериализация в JSON

Автор: IT Sectr Публикувано: 2026-03-15 Време за четене: 12 мин

kotlinx.serialization — мултиплатформена библиотека от JetBrains за преобразуване на Kotlin обекти в JSON, ProtoBuf, CBOR и други формати без използване на рефлексия. За разлика от Gson и Moshi, тя генерира код на сериализатора на етапа на компилация чрез анотация @Serializable, което осигурява висока производителност и типова безопасност. Според GitHub Kotlin/kotlinx.serialization, библиотеката поддържа Kotlin/JVM, Kotlin/Native, Kotlin/JS и Kotlin/Wasm.

Основни точки

  • kotlinx.serialization — compile-time сериализация: кодът се генерира на етапа на компилация, рефлексия не се използва
  • @Serializable — основната анотация, която стартира генерирането на сериализатор за класа
  • Json {} builder — конфигурация на JSON чрез Json { ignoreUnknownKeys = true; prettyPrint = true }
  • Мултиплатформеност — библиотеката работи на JVM, Native, JS и Wasm без промяна на API
  • Потребителски сериализатори — чрез интерфейса KSerializer за нестандартни формати данни

Какво е kotlinx.serialization

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.

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

JSON — най-популярният формат в kotlinx.serialization. За сериализация на обект е достатъчно да поставите анотацията @Serializable върху data class и да извикате Json.encodeToString(). За десериализация — Json.decodeFromString() с посочване на типа. Библиотеката автоматично обработва null полета, списъци, вложени обекти и изброявания. Всички полета на класа са по подразбиране задължителни, освен ако не е посочено друго.

Конфигурацията на JSON се извършва чрез Json {} builder. В конструктора могат да се предадат ignoreUnknownKeys = true за пропускане на неизвестни полета при десериализация, prettyPrint = true за форматиран изход, coerceInputValues = true за преобразуване на некоректни стойности в стойности по подразбиране. Също така са достъпни настройките encodeDefaults (сериализация на полета със стойности по подразбиране) и classDiscriminator (име на полето за полиморфна сериализация).

kotlin
// Пример за сериализация и десериализация на 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

Sealed class — един от най-мощните случаи на използване на kotlinx.serialization. Библиотеката поддържа полиморфна сериализация за йерархии от sealed class без допълнителна конфигурация: достатъчно е да маркирате sealed class и всички негови наследници с анотация @Serializable. При сериализация се добавя полето „type” (конфигурируемо чрез classDiscriminator), въз основа на което при десериализация се определя конкретният тип.

kotlin
// Полиморфна сериализация на 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”) на произволна стойност, очаквана от сървъра.

Анотации на kotlinx.serialization: пълен преглед

Библиотеката предоставя набор от анотации за фина настройка на сериализацията. Основната е @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 е удобна за полета, които не трябва да се изпращат на сървъра — например временни изчислени стойности или кеш.

@Required като алтернатива на nullable полета

По подразбиране всички полета в kotlinx.serialization са задължителни. Ако поле може да липсва в JSON, трябва да го направите nullable (String?) или да зададете стойност по подразбиране (val name: String = „”). Съществуват обаче ситуации, когато полето не е nullable в Kotlin, но може да липсва в JSON поради версиониране на API. В този случай @Required хвърля SerializationException при липса на поле, докато стойността по подразбиране попълва default без грешка.

Потребителски сериализатори: KSerializer и ръчно управление

KSerializer — интерфейс, който имплементират всички сериализатори в kotlinx.serialization. Ако стандартното генериране на код не е подходящо (например за работа с Date, Bitmap или специфичен двоичен формат), можете да напишете свой собствен сериализатор. За целта трябва да имплементирате методите serialize() и deserialize(), както и да предоставите дескриптор — описание на структурата за схемата на формата.

Потребителските сериализатори се свързват по два начина: чрез анотацията @Serializable(with = MySerializer::class) за свързване с конкретен клас или глобално чрез Json { serializersModule = ... } за свързване с всички инстанции на типа. Вторият начин е за предпочитане за вградени типове (Date, UUID), за да не се налага да пишете анотация на всяко поле.

kotlin
// Потребителски сериализатор за 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) } }.

Формати на сериализация: JSON, ProtoBuf, CBOR, HOCON

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.

ФорматМодулТипСхемаТипично приложение
JSONkotlinx-serialization-jsonТекстовПо изборREST API, съхранение на данни
ProtoBufkotlinx-serialization-protobufДвоиченЗадължителна (.proto)Микросървиси, gRPC
CBORkotlinx-serialization-cborДвоиченПо изборIoT, мобилни устройства
HOCONkotlinx-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.

Типични грешки при работа с kotlinx.serialization

Първа грешка — игнориране на неизвестни ключове при десериализация. Ако сървърът е добавил ново поле и имате 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 се различава от Gson и Moshi?

kotlinx.serialization използва генериране на код по време на компилация чрез KSP, докато Gson и Moshi използват рефлексия по време на изпълнение. Това дава предимство в производителността (3-5 пъти по-бързо от Gson) и типова безопасност. Gson сериализира всяко поле без анотация, което може да доведе до изтичане на данни. kotlinx.serialization изисква изрична анотация @Serializable, което е по-безопасно. Moshi също поддържа codegen, но само за JVM и Android.

Поддържа ли kotlinx.serialization Kotlin Multiplatform?

Да, kotlinx.serialization е официалната мултиплатформена библиотека на JetBrains. Работи на Kotlin/JVM (Android, Backend), Kotlin/Native (iOS), Kotlin/JS (Web, React) и Kotlin/Wasm. API е единен за всички платформи: @Serializable + Json.encodeToString() работи еднакво навсякъде. За iOS не са необходими допълнителни настройки — Kotlin/Native компилира сериализирания код в роден двоичен файл.

Как да обработваме null полета в JSON?

Nullable полета (String?) се десериализират като null, ако стойността в JSON липсва или е посочена като null. За non-nullable полета (String) без стойност по подразбиране липсата на поле в JSON ще предизвика SerializationException. Ако искате null стойности да не влизат в JSON, конфигурирайте Json { encodeDefaults = false }. Това ще изключи от изхода всички полета, равни на default (включително null за nullable).

Какво да правим, ако сървърът изпраща snake_case полета?

Използвайте @SerialName(„snake_case_name”) на всяко поле, чието име се различава от формата на Kotlin. Алтернативно, за Kotlin 2.0+ е наличен Json { namingStrategy = JsonNamingStrategy.SnakeCase } — автоматично преобразуване camelCase ↔ snake_case. Тази настройка се прилага към всички полета едновременно. Ако е необходимо частично персонализиране, комбинирайте @SerialName с глобалната стратегия.

Може ли да се сериализира Kotlin Flow или coroutine?

Не, Flow и корутините не могат да бъдат директно сериализирани — те представляват асинхронно изпълнение, а не данни. За прехвърляне на данни от Flow трябва да ги съберете в колекция чрез .toList() в корутина и да сериализирате колекцията. По подобен начин, Job, Deferred или Continuation не могат да бъдат сериализирани. Сериализирайте само data class — модели на данни без поведенческа логика.

Обобщение

  • kotlinx.serialization — compile-time сериализация чрез @Serializable, без рефлексия, с производителност до 5 пъти по-висока от Gson
  • @Serializable, @SerialName, @Transient — ключови анотации за конфигуриране на сериализация на полета и класове
  • Json {} builder конфигурира JSON: ignoreUnknownKeys, prettyPrint, coerceInputValues, encodeDefaults
  • Sealed class и полиморфна сериализация — безпроблемна поддръжка на типови йерархии без допълнителен код
  • KSerializer — интерфейс за потребителски сериализатори на нестандартни типове (Date, Bitmap, UUID)
  • Четири формата: JSON, ProtoBuf, CBOR, HOCON — свързват се с модули, API единен за всички
  • Мултиплатформеност — единен код за JVM, Native, JS и Wasm; критично за KMM и споделени модули

Ще разработим мобилно приложение под ключ

IT Sectr създава iOS и Android приложения за стартъпи и бизнеси от 2017 г. Ще ви консултираме и ще предложим най-доброто решение.

Обсъдете проекта

Прочетете също