JavaRush /Курси /Kotlin SELF /Під’єднання kotlinx.serialization та анотація @Serializab...

Під’єднання kotlinx.serialization та анотація @Serializable

Kotlin SELF
Рівень 47 , Лекція 2
Відкрита

1. А чи потрібна серіалізація?

Перше бажання новачка цілком логічне: «А можна я просто зроблю println(user) і запишу це у файл? Там же вже є текст!» І ось тут починається класична пастка: текст — це не завжди дані. Точніше, текст може бути даними, але лише тоді, коли має чіткі правила формату, і ми зможемо відновити об’єкт без ворожіння на кавовій гущі.

toString() у data class справді повертає гарний рядок такого вигляду: User(id=1, name=Ada). Але це не контракт формату, а зручність для налагодження. У наступній версії програми ви перейменуєте поле, зміните порядок параметрів, додасте нове поле — і старі файли стануть «артефактами давньої цивілізації», які ваша ж програма вже не зможе прочитати.

Тому нам і потрібна серіалізація: ми хочемо перетворювати об’єкт на формат зберігання або обміну, а потім відновлювати його назад за зрозумілими правилами. У нашому випадку обрано JSON — і це добра новина. JSON легко читається «очима», зберігається у файлі як звичайний текст, і його розуміють майже всі мови.

2. Як влаштований kotlinx.serialization

kotlinx.serialization — це не просто набір функцій «зроби мені JSON». Ключова частина цієї технології в тому, що Kotlin під’єднує компіляторний плагін, який уміє генерувати код серіалізації для ваших класів. Тобто ви пишете @Serializable data class User(...), а компілятор створює «невидимих помічників», які знають, як цей User перетворити на JSON-структуру — і як відновити його назад.

Звучить трохи містично, але ідея дуже практична: ручне написання серіалізації майже завжди закінчується помилками. Особливо тоді, коли у вас є вкладені об’єкти, списки, nullable-поля та значення за замовчуванням.

Невеличка схема, щоб «укласти в голові», що і де відбувається:

flowchart TD
    A[Ваш код: data class + @Serializable] --> B[Компілятор Kotlin + serialization plugin]
    B --> C[Згенерований код серіалізатора]
    C --> D[Runtime-бібліотека kotlinx-serialization-json]
    D --> E[JSON текст / JSON дерево]

3. Під’єднання в Gradle

Під’єднання серіалізації майже завжди «ламається» в новачків з однієї з двох причин: або додали бібліотеку, але забули компіляторний плагін, або навпаки. Тому тримайте в голові просте правило: потрібні і залежність, і плагін. І так, це один із тих моментів, коли Gradle ніби натякає: «я не злий — я просто конфігурація».

Під’єднуємо плагін серіалізації

У Kotlin/JVM-проєкті на Gradle (з build.gradle.kts) зазвичай під’єднують плагін серіалізації поруч з основним Kotlin-плагіном. Важливо, щоб версія плагіна серіалізації збігалася з версією Kotlin-плагіна проєкту (у нашому курсі — Kotlin 2.3). Якщо версії роз’їдуться, ви можете отримати дуже дивні помилки, від яких навіть компілятор розгубиться.

Приклад фрагмента build.gradle.kts (коротко, лише суть):

plugins {
    kotlin("jvm") version "2.3.0"
    kotlin("plugin.serialization") version "2.3.0"
}

Якщо у вашому проєкті версія Kotlin не "2.3.0", а інша (наприклад, "2.3.10"), то і тут має бути така сама.

Під’єднуємо залежність для JSON-модуля

Тепер під’єднаємо runtime-бібліотеку, яка дає нам Json і все, що пов’язано з JSON-специфікою. Версії kotlinx.serialization живуть своїм життям, тому конкретне число з часом може змінитися. Але принцип один: додаємо kotlinx-serialization-json у dependencies.

repositories {
    mavenCentral()
}

dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.0")
}

Якщо ви питаєте: «А точно "1.7.0"?» — для навчального прикладу важливіша структура, ніж конкретна цифра. Версію можна змінювати, але плагін і Kotlin мають «дружити» між собою. Інакше вечірка закінчиться раніше, ніж почнеться.

Синхронізація проєкту

Після правок у Gradle-файлі потрібно виконати синхронізацію (в IntelliJ IDEA зазвичай з’являється кнопка «Load Gradle Changes» / «Sync»). Без цього IDE може продовжувати показувати Unresolved reference, хоча ви все вже написали правильно. Це не містика — просто Gradle ще не завантажив залежності й не перебудував модель проєкту.

4. Анотація @Serializable і моделі

Слово «анотація» легко сприймається як «щось для дорослих розробників, які їдять винятки на сніданок». Але по суті анотація — це просто мітка в коді. Вона не «виконується» як функція. Вона підказує інструментам (компілятору, бібліотекам, IDE): «ось цей елемент треба обробити особливим чином».

У нашому випадку @Serializable — це мітка: «для цього класу потрібно згенерувати серіалізатор». А далі вже компіляторний плагін робить свою частину роботи.

Мінімальний приклад

import kotlinx.serialization.Serializable

@Serializable
data class User(
    val id: Int,
    val name: String
)

Тут важливо, що @Serializable ставиться на клас. Ставити її на окремі поля сьогодні не потрібно (тонкі налаштування формату будуть пізніше).

Модель Expense

Щоб приклади не були «у вакуумі», продовжимо умовний практичний консольний застосунок — нехай це буде простий трекер витрат. Раніше ми могли зберігати витрати в MutableList<Expense> і виводити їх у консоль. Тепер готуємося до того, щоб зберігати стан у JSON-файл (але сам encode/decode буде вже в наступній лекції).

Почнемо з моделі витрати:

import kotlinx.serialization.Serializable

@Serializable
data class Expense(
    val id: Int,
    val title: String,
    val amount: Int
)

Поля максимально прості: Int і String. Це зроблено навмисно: що простіша модель на старті, то менше шансів «упіймати» проблему через тип, якого серіалізація не розуміє.

Модель AppState

Тепер додамо модель стану застосунку, щоб потім зберігати не лише список витрат, а й, наприклад, наступний id:

import kotlinx.serialization.Serializable

@Serializable
data class AppState(
    val nextId: Int,
    val expenses: List<Expense>
)

Зверніть увагу на важливу річ: якщо AppState містить Expense, то Expense теж має бути серіалізовним. Інакше компілятор праведно обурюватиметься: «Я не знаю, як серіалізувати поле такого типу».

Nullable-поля та значення за замовчуванням

На практиці дані часто бувають неповними: коментар може бути відсутнім, додатковий опис — порожнім, а якесь поле може з’явитися пізніше, коли ви розширите програму. Тому nullable-типи та значення за замовчуванням — нормальний інструмент. Просто важливо розуміти, що вони впливають на те, як дані виглядатимуть у JSON (точні налаштування ми розбиратимемо в лекції про Json { ... }).

Додамо до витрати необов’язковий коментар і валюту зі значенням за замовчуванням:

import kotlinx.serialization.Serializable

@Serializable
data class Expense(
    val id: Int,
    val title: String,
    val amount: Int,
    val currency: String = "USD",
    val note: String? = null
)

Сенс такий: старі записи можуть не мати note, а валюта за замовчуванням є завжди. Це хороший крок до «живучого» формату даних, який не ламається від кожного чха.

5. Швидка перевірка та структура проєкту

Іноді здається, що все під’єднано, але всередині проєкту плагін не активувався (або Gradle не синхронізувався). Потрібен простий тест, який перевіряє: «компілятор справді згенерував серіалізатор».

Перевіряємо, що плагін справді працює

Один із таких тестів — звернутися до serializer() у класу. Якщо серіалізатор згенеровано, код скомпілюється.

import kotlinx.serialization.Serializable

@Serializable
data class User(val id: Int, val name: String)

fun main() {
    val s = User.serializer()
    println(s.descriptor.serialName) // User
}

Якщо у вас тут помилка компіляції, то причина майже завжди в під’єднанні: або забули kotlin("plugin.serialization"), або не завантажилася залежність, або проєкт не пересинхронізовано. І так, це той рідкісний випадок, коли «перезібрати проєкт» — не шаманство, а цілком реальна діагностика.

Де зберігати моделі й як не влаштувати звалище

Коли ви додаєте серіалізацію, з’являється спокуса «розмазати» @Serializable по всьому проєкту, а класи тримати поруч із main, бо «так швидше». Швидше — так. Але лише до першого моменту, коли ви не зможете знайти, де у вас Expense, а де AppState, і чому вони в трьох місцях відрізняються.

Хороша звичка вже зараз — тримати моделі в окремому пакеті, наприклад app.model або tracker.model. Тоді файли виглядатимуть передбачувано: Expense.kt, AppState.kt, і в кожному — один головний тип.

До того ж, коли модель лежить окремо, набагато простіше помітити: «Ага, я додав нове поле — отже, формат даних змінився». Це корисно навіть у навчальному проєкті.

6. Типові помилки під час під’єднання kotlinx.serialization

Помилка № 1: додали залежність, але забули плагін серіалізації.
Дуже частий сценарій: ви під’єднали implementation("...kotlinx-serialization-json..."), написали @Serializable, а IDE все одно свариться або User.serializer() не знаходиться. Причина в тому, що без kotlin("plugin.serialization") компілятор не генерує серіалізатори — сама бібліотека не «витягує» весь процес. Лікується додаванням плагіна та синхронізацією Gradle.

Помилка № 2: версії Kotlin-плагіна та serialization-плагіна не збігаються.
Тут проблема в тому, що помилка може виглядати «не в тему»: іноді це дивні повідомлення компілятора або падіння збирання на етапі, який не пов’язаний із вашим кодом. Практичне правило просте: якщо проєкт на Kotlin 2.3.x, то і kotlin("plugin.serialization") має бути 2.3.x.

Помилка № 3: серіалізуємо клас, у якого є поле складного типу, не підтриманого «з коробки».
Наприклад, ви додали поле типу File, Path, Instant, або якийсь ваш клас без @Serializable. Компілятор скаже: «Не знаю, як це серіалізувати». Це не примха: серіалізація має мати чіткий алгоритм для кожного поля. На старті тримайте моделі простими: числа, рядки, списки, nullable. Складні типи або «загортаються» в простіший вигляд, або потребують окремого підходу (але це не тема сьогоднішньої лекції).

Помилка № 4: імпортували не ту Serializable.
У Kotlin/Java світі є кілька Serializable. Нам потрібна саме kotlinx.serialization.Serializable. Якщо ви випадково імпортували java.io.Serializable, то компіляторний плагін серіалізації цього не «побачить», і далі почнуться загадки рівня «чому воно не працює, я ж написав Serializable». Перевіряйте імпорт — це дрібниця, але одна з найпідступніших.

Помилка № 5: очікування, що @Serializable автоматично почне зберігати дані у файл.
Анотація робить клас «придатним до серіалізації», але сама по собі нічого не записує. Запис і читання JSON — це окремий крок: кодек Json, функції encode/decode та файлові операції. Сьогодні ми закладаємо фундамент (моделі + під’єднання), а практичний процес «об’єкт ↔ JSON ↔ файл» буде наступним етапом.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ