JavaRush /Courses /Kotlin SELF /Adding kotlinx.serialization and the @Serializable annota...

Adding kotlinx.serialization and the @Serializable annotation

Kotlin SELF
Level 47, Lesson 2
Available

1. Do we even need serialization?

A beginner’s first impulse is totally logical: “Can I just do println(user) and write it to a file? There’s already text there!” And this is where the classic trap begins: text is not always data. More precisely, text can be data, but only if it has clear formatting rules and we can later reconstruct the object back without guesswork.

toString() on a data class really does produce a nice-looking string like User(id=1, name=Ada). But that is not a format contract; it’s a convenience for debugging. In the next version of the program, you will rename a field, change the parameter order, add a new field—and old files will turn into “artifacts of an ancient civilization” that your own program can’t read.

That’s why we need serialization: we want to convert an object into a storage/exchange format and then restore it back using clear rules. In our case, JSON is chosen, and that’s good news: JSON is human-readable, stored in a file as regular text, and understood by almost all languages.

2. How kotlinx.serialization works

kotlinx.serialization is not just a set of functions “make me JSON.” A key part of this technology is that Kotlin enables a compiler plugin that can generate serialization code for your classes. That is, you write @Serializable data class User(...), and the compiler creates “invisible helpers” that know how to turn this User into a JSON structure and back.

It sounds mystical, but the idea is very practical: writing serialization by hand almost always ends in errors, especially when you have nested objects, lists, nullable fields, and default values.

A small diagram to help “make it click” what happens where:

flowchart TD
    A[Your code: data class + @Serializable] --> B[Kotlin compiler + serialization plugin]
    B --> C[Generated serializer code]
    C --> D[Runtime library kotlinx-serialization-json]
    D --> E[JSON text / JSON tree]

3. Adding it in Gradle

Adding serialization almost always breaks for beginners for one of two reasons: either they add the library but forget the compiler plugin, or the other way around. So keep a simple rule in mind: you need both the dependency and the plugin. And yes, this is one of those moments when Gradle reminds you: “I’m not evil, I’m just config.”

Add the serialization plugin

In a Kotlin/JVM Gradle project (with build.gradle.kts), the serialization plugin is usually added next to the main Kotlin plugin. It’s important that the serialization plugin version matches the Kotlin plugin version of the project (in our course—Kotlin 2.3). If the versions drift apart, you may get very strange errors that even the compiler will be embarrassed about.

An example fragment of build.gradle.kts (short, essentials only):

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

If your Kotlin version in the project is not "2.3.0" but another one (for example, "2.3.10"), then this one should be the same.

Add the dependency on the JSON module

Now let’s add the runtime library that provides Json and everything related to JSON specifics. Versions of kotlinx.serialization have their own lifecycle, so the exact number may change over time—but the principle is the same: add kotlinx-serialization-json to dependencies.

repositories {
    mavenCentral()
}

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

If you’re asking “is it definitely "1.7.0"?”—for a learning example, structure is more important than the exact digit. You can change the version, but the plugin and Kotlin must be compatible with each other, otherwise the party ends before it even starts.

Project sync

After editing the Gradle file, you need to run synchronization (in IntelliJ you usually get a “Load Gradle Changes” / “Sync” button). Without it, the IDE may continue showing Unresolved reference, even though you wrote everything correctly. This is not mysticism—Gradle just hasn’t downloaded the dependencies and rebuilt the project model yet.

4. The @Serializable annotation and models

The word “annotation” can easily sound like “something for grown-up developers who eat exceptions for breakfast.” But essentially, an annotation is just a marker in code. It doesn’t “execute” like a function. It tells tools (the compiler, libraries, the IDE): “this element should be processed in a special way.”

In our case, @Serializable is a marker: “generate a serializer for this class.” Then the compiler plugin does its part of the work.

Minimal example

import kotlinx.serialization.Serializable

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

What matters here is that @Serializable is placed on the class. Putting it on individual fields is not needed today (fine-grained format settings will come later).

The Expense model

So the examples are not “in a vacuum,” let’s continue our hypothetical practical console app—let it be a simple expense tracker. Previously, we could store expenses in MutableList<Expense> and print them to the console. Now we are preparing to save the state to a JSON file (but the actual encode/decode will be in the next lecture).

Let’s start with the expense model:

import kotlinx.serialization.Serializable

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

The fields are as simple as possible: Int and String. This is intentional: the simpler the model at the start, the lower the chance you’ll “run into” a problem because of a type that serialization doesn’t understand.

The AppState model

Now let’s add an application state model so that later we can store not only the list of expenses, but also, for example, the next id:

import kotlinx.serialization.Serializable

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

Pay attention to an important thing: if AppState contains Expense, then Expense must also be serializable. Otherwise, the compiler will (rightfully) complain: “I don’t know how to serialize a field of this type.”

Nullable fields and default values

In practice, data is often incomplete: a comment may be missing, an additional description may be empty, and some field may appear later when you expand the program. So nullable types and default values are normal tools—it’s just important to understand that they affect how the data will look in JSON (we’ll discuss the exact settings in the lecture about Json { ... }).

Let’s add an optional note and a currency with a default value to an expense:

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
)

The idea is: old records may not have note, while the default currency is always present. This is a good step toward a “resilient” data format that doesn’t break from every little change.

5. Quick check and project structure

Sometimes it feels like everything is connected, but inside the project the plugin didn’t activate (or Gradle didn’t sync). You need a simple test that checks: “did the compiler really generate the serializer?”

Verify that the plugin actually works

One such test is to call serializer() on the class. If the serializer is generated, the code compiles.

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
}

If you get a compilation error here, the cause is almost always in the setup: either you forgot kotlin("plugin.serialization"), or the dependency didn’t download, or the project wasn’t re-synced. And yes, this is that rare case where “rebuild the project” is not shamanism, but real diagnostics.

Where to store models and how not to create a mess

When you add serialization, it’s tempting to smear @Serializable all over the project, and keep classes next to main, because “it’s faster that way.” It is faster—until the first moment when you can’t find where your Expense is, where your AppState is, and why they differ in three places.

A good habit to build right now: keep models in a separate package, for example app.model or tracker.model. Then your files will look predictable: Expense.kt, AppState.kt, and each contains one main type.

Also, when the model lives separately, it’s much easier to notice: “okay, I added a new field—so the data format has changed.” This is useful even in a learning project.

6. Common mistakes when adding kotlinx.serialization

Error No. 1: you added the dependency but forgot the serialization plugin.
A very common scenario: you added implementation("...kotlinx-serialization-json..."), wrote @Serializable, but the IDE still complains or User.serializer() can’t be found. That’s because without kotlin("plugin.serialization"), the compiler doesn’t generate serializers—the library alone can’t carry the whole process. Fix it by adding the plugin and syncing Gradle.

Error No. 2: the Kotlin plugin version and the serialization plugin version don’t match.
The painful part is that the error may look “unrelated”: sometimes it’s weird compiler messages or a build failure at a stage that isn’t tied to your code. The practical rule is simple: if the project uses Kotlin 2.3.x, then kotlin("plugin.serialization") must also be 2.3.x.

Error No. 3: you serialize a class that has a field of a complex type not supported out of the box.
For example, you added a field of type File, Path, Instant, or some class of your own without @Serializable. The compiler will say: “I don’t know how to serialize this.” This isn’t a whim: serialization must have a clear algorithm for every field. At the start, keep models simple: numbers, strings, lists, nullable. Complex types are either wrapped into a simpler representation or require a separate approach (but that’s not today’s topic).

Error No. 4: you imported the wrong Serializable.
In the Kotlin/Java world there are several Serializable types. We need kotlinx.serialization.Serializable. If you accidentally imported java.io.Serializable, the serialization compiler plugin won’t “see” it, and then you get mysteries like “why doesn’t it work, I wrote Serializable.” Check the import—this is small, but one of the most insidious issues.

Error No. 5: expecting @Serializable to automatically start saving data to a file.
The annotation makes the class “ready for serialization,” but it doesn’t write anything by itself. Writing/reading JSON is a separate step: the Json codec, encode/decode functions, and file operations. Today we are laying the foundation (models + setup), and the practical “object ↔ JSON ↔ file” pipeline will be the next stage.

1
Task
Kotlin SELF, level 47, lesson 2
Locked
Expense categories
Expense categories
1
Task
Kotlin SELF, level 47, lesson 2
Locked
Type hint
Type hint
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION