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 обычно появляется кнопка “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 ↔ файл» будет следующим этапом.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ