JavaRush/Courses/Kotlin SELF/Introduction to serialization

Introduction to serialization

Open

1. Introduction

When we write a program, we usually think like this: “I have a list of expenses, each expense has an amount and a category, I can calculate the total.” All of that is true—but only while the program is running. An object in memory is a data structure living inside a process, and it’s not designed to travel into the real world: into a file, over the network, or into another program. As soon as the application exits, all our List<Expense> vanish into digital oblivion (almost like browser tabs that “were definitely important”).

Let’s lock in a simple model. While the program runs, data exists in RAM as Kotlin/JVM objects. But to keep it between runs, we need a portable form—usually a string or bytes. And this is where the key word of today’s lecture appears: serialization.

Imagine we have an expense model:

data class Expense(
    val id: Int,
    val title: String,
    val amountCents: Int,
    val category: String
)

Such objects live perfectly in a list, get sorted, filtered, and used in calculations. But we can’t “put an object directly into a file” as easily as file.writeText(expense)—a file understands text (or bytes), not our Kotlin types.

2. Serialization and deserialization

When we say “save data”, what we really want is to pull off a specific trick: convert an object into a portable form and back. In programming, this has long been formalized with two terms.

Serialization is converting an object (or a group of objects) into a storage/transfer format: a string or a byte sequence.

Deserialization is the reverse operation: reconstructing an object from that string/bytes.

In everyday life, it’s like moving. While you live in an apartment, things are placed “however it’s convenient”: mugs on a shelf, socks in a drawer, and your charger is always missing. But to move everything to another apartment, you pack your things into boxes by clear rules (and label them if you’re an optimist). Serialization is packing; deserialization is unpacking.

The scheme usually looks like this:

flowchart TB
    A["Kotlin objects in memory
List⟨Expense⟩"] --> B["Serialization
object → string/bytes"] B --> C["Storage/transfer
file/network/DB"] C --> D["Deserialization
string/bytes → object"] D --> E["Kotlin objects in memory
List⟨Expense⟩"]

Why does this matter? Because it separates “data as meaning” from “data as representation”. Our Expense is meaning, and the file is representation.

3. Why toString() is not suitable

At this point almost everyone feels tempted: “Wait, but println(expense) prints something readable. So we can save that to a file and then read it back!”

Let’s look. If we have a data class, Kotlin generates a decent toString() automatically:

data class Expense(val id: Int, val title: String, val amountCents: Int, val category: String)

fun main() {
    val e = Expense(1, "Coffee", 299, "food")
    println(e) // Expense(id=1, title=Coffee, amountCents=299, category=food)
}

It almost looks like a “data format”. But the problem is that toString() is not a storage contract. It’s a string for a human (primarily for debugging), not a promise that it can be reliably parsed back.

There are several reasons why using toString() as a “format” is dangerous:

  • First, the toString() format can change. Today Kotlin prints Expense(id=..., ...), tomorrow (or future you) overrides toString() for prettiness—and that’s it, your old files can no longer be read.
  • Second, toString() doesn’t care about escaping. If title contains commas, parentheses, or a newline, the string becomes ambiguous. And yes, sooner or later a user will enter something like Cheese (very tasty) and you’ll learn a lot about parsing.
  • Third, the toString() string can’t properly describe complex cases: null, nested lists, maps, new fields, and version compatibility.

With collections it’s similar: a list has a string representation, and by default it resembles joinToString(). Kotlin documentation explicitly notes that calling joinToString() with default settings yields a result similar to a collection’s toString(). But it’s still “readable”, not “reliably parseable”.

4. Why hand-assembling JSON is dangerous

When someone realizes that toString() won’t work, they often move to the next confidence level: “I’ll write a function that builds a string in the format I need.” And somewhere around that moment, programming starts quietly laughing into its fist.

An example of “manual serialization” (heavily simplified):

data class Expense(val id: Int, val title: String, val amountCents: Int, val category: String)

fun expenseToJsonLike(e: Expense): String {
    return """{"id":${e.id},"title":"${e.title}","amountCents":${e.amountCents},"category":"${e.category}"}"""
}

fun main() {
    val e = Expense(1, "Coffee", 299, "food")
    println(expenseToJsonLike(e))
    // {"id":1,"title":"Coffee","amountCents":299,"category":"food"}
}

As long as title and category don’t contain quotes, newlines, and other “little joys of life”, it seems to work. But as soon as someone enters:

  • Coffee "to go" (with quotes),
  • Sushi\nfor two (with a newline),
  • or simply C:\Temp (with backslashes),

your string will stop being valid, because JSON requires escaping.

And that’s only the first problem. Next come commas, spaces, nested objects, arrays, null, default values, field ordering, and “oops, we forgot to close a curly brace”.

The moral is simple: you can build strings by hand, but it’s like trying to build an airplane out of duct tape. At some point it might even take off—but you won’t want to be a passenger.

That’s why in real projects serialization is almost always done by a library that:

  • escapes strings correctly,
  • strictly follows the format rules,
  • can encode collections and nested structures,
  • can decode back into types.

Today we won’t add a library (that will come later in the course), but it’s important to understand the motivation: a library here is not “fashion”, it’s protection against manual mistakes.

5. JSON and the data contract

Why JSON

When we choose a serialization format, we choose a compromise: between convenience, portability, size, and strict rules. JSON became popular not because it’s perfect, but because it balances all of these fairly well.

JSON is a text format that:

  • First, is easy to read with your eyes. This matters not only “for aesthetics”. When your file breaks, being able to open it in a text editor and understand “what’s even in there” is a superpower.
  • Second, is supported almost everywhere. Kotlin, Java, JavaScript, Python—everyone can read/write JSON. So JSON is convenient for exchanging data between systems and for “on-disk” storage.
  • Third, is structured enough not to turn into a “wall of text”. Unlike something like “let’s save everything as a string separated by ;”, JSON immediately defines a shape: object, fields, arrays. Yes, strict rules can be annoying (quotes, commas, null without quotes), but they are exactly what saves us from chaos.

Finally, JSON fits well with our data model: “an object with fields” and “a list of objects” map naturally to JSON objects {...} and arrays [...]. We’ll take a detailed look at JSON structure in one of the next lectures; for today an intuitive picture is enough.

Data contract

When you save data, you’re essentially making a deal with your future self. Sometimes—with a colleague. Sometimes—with another program. This deal is called a data contract.

A contract answers questions like: what fields an object has, what they are called, what types they have, which fields are required, and which can be missing.

For example, for our Expense the contract could sound like this (in plain words):

  • an expense has id (a number),
  • title (a string),
  • amountCents (a number),
  • category (a string).

Why is this fundamental? Because deserialization is not “reconstruction magic”. It’s a strict mapping: we read text and try to build an object. If the contract is violated, reading may fail or produce incorrect data.

Let me show a real-life example. Suppose you saved an expense like this:

val text = """{"id":1,"title":"Coffee","amountCents":299,"category":"food"}"""
println(text)

And then a month later you decided to rename the field amountCents to amount (because “it’s shorter”). The code got prettier, but old files still contain amountCents, while the new code expects amount. That’s a contract conflict: data “from the past” no longer matches the model “from the present”.

That’s why any data storage format requires discipline. Serialization is not only the technique of “how to write”, but also the design of “what data is considered valid”.

6. What to store in our app

Mini sketch of the file structure

When you add persistence to an app for the first time, it’s useful to pause for a second and formulate: what exactly do we want to store between runs? This helps avoid mixing “data” with “screen output”.

For our expense tracker it makes sense to store the list of expenses and, possibly, a few settings. For example, the currency, the next id, or the category list (if we make categories configurable). Today we won’t go deep, but at the idea level it might look like this:

Data file (for example, data/expenses.json)
└── top-level object
    ├── nextId: number
    ├── expenses: expense array
    └── currency: string (optional)

In JSON it could look roughly like this (this is just an example shape, not the “only correct” option):

fun main() {
    val demoJson = """
        {
          "nextId": 3,
          "currency": "USD",
          "expenses": [
            { "id": 1, "title": "Coffee", "amountCents": 299, "category": "food" },
            { "id": 2, "title": "Transit", "amountCents": 250, "category": "transport" }
          ]
        }
    """.trimIndent()

    println(demoJson)
}

What matters here is not the syntax, but the idea: JSON lets you store both a list of objects and additional fields alongside it, without turning everything into a mess.

Where a serialization library fits

Now that we have the motivation, we can honestly say: “OK, JSON is great, but who will write the code that turns List<Expense> into that string and back?”

Answer: a serialization library.

In the Kotlin world, a popular option is kotlinx.serialization, because it works nicely with data class, supports Kotlin types, and behaves predictably. But the mere fact “we’ll use a library” is less important today than understanding the role of that library.

The library’s role is to be your “translator” between two worlds:

  • the world of Kotlin objects (strict types, Int, String, List<Expense>, nullable fields),
  • and the world of text (a JSON string, where everything must be correctly placed, escaped, and structured).

If the translator is good, you write less code, make fewer mistakes, and your files don’t break because someone put a quote in the purchase name.

7. Common mistakes

Mistake #1: treating toString() as a data storage format.
toString() often does look “almost like data”, especially for a data class. But it’s a human-readable representation, not a stable machine contract. Today it’s one thing; tomorrow you changed the model or overrode the method, and old files stop being readable.

Mistake #2: “quickly throwing together a JSON string” without escaping.
Manually assembling JSON via """...$value...""" almost inevitably breaks on quotes, newlines, and backslashes. With test data it seems fine, and then a user types a name with "..."—and the format falls apart. It’s like testing a parachute by dropping it off a stool.

Mistake #3: mixing “data” and “pretty output”.
Sometimes you want to store already formatted strings in a file, like "Coffee — 2.99 USD". That’s convenient for printing, but inconvenient for calculations and changes. You should store raw values (for example, amountCents as a number) and format for humans at output time.

Mistake #4: not thinking about the contract and renaming fields “however you feel”.
Once data starts living in a file, it doesn’t care that you renamed amountCents to amount. The file stays old. If the contract changes without a strategy, reading fails or starts interpreting data incorrectly.

Mistake #5: believing that “since JSON is text, it’s always safe”.
JSON is easier to fix by hand than a binary format, but it doesn’t protect you from logical errors: wrong types, missing fields, null where you didn’t expect it, or extra fields from old versions. A text format makes diagnostics easier, but it doesn’t remove the need for validation and careful design.

1
Task
Kotlin SELF,  47 level,  0 lecture
Locked
Expense contract
Expense contract
1
Task
Kotlin SELF,  47 level,  0 lecture
Locked
Dangerous JSON
Dangerous JSON
1
Task
Kotlin SELF,  47 level,  0 lecture
Locked
Saving Quotes
Saving Quotes
1
Task
Kotlin SELF,  47 level,  0 lecture
Locked
Expense archive
Expense archive
Комментарии
  • популярные
  • новые
  • старые
Для того, чтобы оставить комментарий Вы должны sign in
This page has no comments yet