JavaRush /Courses /Kotlin SELF /JSON Structure and Its Relationship to Kotlin Types

JSON Structure and Its Relationship to Kotlin Types

Kotlin SELF
Level 47, Lesson 1
Available

1. JSON as a language of “boxes”: what kinds of values exist

Before trying to “plug JSON into an app,” it helps to take a minute to imagine that JSON is a very simple data description language with only a few kinds of “boxes.” If you learn to quickly identify which box you’re looking at (object, array, primitive, or null), you’ll drastically reduce the number of future mistakes. And yes, this is one of those rare cases where carefully reading brackets can genuinely save hours of debugging.

In JSON, there are only four forms of values:

  • object { ... }
  • array [ ... ]
  • primitive: string, number, boolean
  • null as a separate value

To “touch” this in Kotlin, for now we’ll store JSON as a regular string (this is fine: JSON is essentially text).

fun main() {
    val obj = """{"id": 1, "name": "Ada"}"""
    val arr = """[1, 2, 3]"""
    val prim = """"hello""""
    val nil = "null"

    println(obj)   // {"id": 1, "name": "Ada"}
    println(arr)   // [1, 2, 3]
    println(prim)  // "hello"
    println(nil)   // null
}

Notice the “double quotes inside the string.” In Kotlin, it’s very convenient to use multiline strings """...""" for JSON, because you almost don’t need to escape quotes there.

2. JSON object: keys and values and how they relate to Kotlin

A JSON object usually looks like a “dictionary-like” set of fields. That’s why people often say “JSON is like an object.” And it really does look like an object… until you remember that JSON knows nothing about classes, methods, or private fields—only data. So, when we map a JSON object to a Kotlin type, we think either about a data class or about Map<String, …>.

In a JSON object:

  • curly braces { ... } denote an object
  • inside are pairs "key": value
  • a key is always a string and always in double quotes
  • pairs are separated by commas

Example of a JSON object for a single expense in our practical console app “Expense Tracker” (roughly: expense tracking):

{
  "id": 10,
  "title": "Coffee",
  "amount": 3.5,
  "category": "food"
}

Now—the key mapping. On the Kotlin side, this most often becomes a model (we already know data class, so we’ll use it as an “honest data container”):

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

Why is this similar?

  • JSON keys ("id", "title") correspond to property names
  • values must be type-compatible (id → Int, title → String, etc.)
  • field order does not have to match (key names matter, not order)

Sometimes it’s convenient to represent a JSON object as Map<String, Any?>, because it’s literally “key-value.” But in practice it’s less type-safe: instead of a clear expense.amount: Double you get “something” like Any? and then you have to sort it out yourself. Still, as a mapping idea this is important: a JSON object is conceptually closest to a Map and to an “object with fields.” We’ve already encountered Map as a “storage of key-value pairs” type in Kotlin.

A small visual diagram:

flowchart LR
    A["JSON object { ... }"] --> B["Kotlin data class (typed model)"]
    A["JSON object { ... }"] --> C["Kotlin Map⟨String, Any?⟩ (dynamic form)"]

And one more quick eye-check: if you see { at the beginning (after spaces), then the top level is an object.

fun main() {
    val text = """ { "id": 1 } """
    val first = text.trim().first()
    println(first) // {
}

3. JSON array: order and its relationship to Kotlin List

After objects, the second most popular creature is the JSON array. It looks like a list of values. It’s easy to build intuition here: square brackets are a “box with elements in order.” And unlike an object, an array has no keys: only values, and their order matters.

A JSON array:

  • starts with [ and ends with ]
  • contains values separated by commas
  • values can be anything (objects, arrays, primitives, null)
  • element order matters

Example: a list of expenses (top level is an array of objects):

[
  { "id": 1, "title": "Coffee", "amount": 3.5, "category": "food" },
  { "id": 2, "title": "Bus",    "amount": 2.0, "category": "transport" }
]

In Kotlin, the most natural mapping is List<Expense> or MutableList<Expense>. Lists in Kotlin are a basic tool for “collections of elements” where order is preserved.

We’re not parsing JSON yet, but we can already keep the “ideal target” in mind: we want to end up with a list of Expense objects.

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

fun main() {
    val expenses = listOf(
        Expense(1, "Coffee", 3.5, "food"),
        Expense(2, "Bus", 2.0, "transport"),
    )

    println(expenses.size)     // 2
    println(expenses[0].title) // Coffee
}

The mapping “JSON array → Kotlin list” is almost always the number one path when you store “many entities of the same kind.” And here’s a subtle point: JSON does not formally forbid mixing types in an array (for example [1, "two", null]), but in applications this is almost always a bad idea. Usually we want an array “of one type by contract,” meaning either a list of expenses, or a list of tag strings, or a list of numbers.

4. JSON primitives: strings, numbers, boolean

When you read JSON, primitives are the things that don’t contain other values inside. They seem simple… until questions start like “is this number definitely an Int, or can it have a fractional part?” and “is this a string "1" or a number 1?”. So it’s better to agree with yourself in advance: primitives are where the data contract is especially important.

In JSON there are three “regular” primitives:

  • string: "text"
  • number: 123, 3.14, -10
  • boolean: true or false

And separately there is null, but we’ll talk about it in the next section.

JSON string ↔ String in Kotlin

A string is always in double quotes:

fun main() {
    val jsonTitle = """"Coffee""""
    println(jsonTitle) // "Coffee"
}

If you see quotes, it’s a string. Even if there are digits inside.

JSON boolean ↔ Boolean in Kotlin

Boolean is written without quotes:

fun main() {
    val jsonActive = "true"
    println(jsonActive) // true
}

JSON numbers ↔ Int/Long/Double in Kotlin

This is where reality begins. In JSON, there is only one “number.” There’s no separate Int, Long, Double. There’s just “number.” So the mapping is a data-contract decision:

  • if the field is conceptually an integer (id, count) then in Kotlin it’s usually Int (or Long if large values are expected)
  • if the field can be fractional (amount, price) then usually Double

For our expense tracker, it’s reasonable:

  • id: Int
  • amount: Double

And a good “eye test” is: if you see a dot, you almost certainly need Double.

fun main() {
    val amountJson = "3.5"
    val idJson = "10"

    println(amountJson) // 3.5
    println(idJson)     // 10
}

One more important point: if you accidentally write a number as a string, you’ll get formally valid JSON, but you’ll break the type contract:

{ "amount": "3.5" }   // string, not a number
{ "amount": 3.5 }     // number

Both variants are JSON-valid, but for your model they are different things.

5. null in JSON and Kotlin: T? and missing fields

The topic of null often looks like “what’s so hard about it,” and then turns into a couple evenings of thinking about why “everything seems the same, but the behavior is different.” So let’s go calmly: JSON can store null as a separate value, and Kotlin can store null only in nullable types (T?). Sounds simple—but there’s another layer: in JSON a field can be completely missing.

Here are three different situations:

{ "nickname": null }     // field exists and is explicitly null
{ "nickname": "Ada" }    // field exists and is a string
{ }                      // there is no nickname field at all

In Kotlin, the most direct mapping for “might be a string or might have no value” is String?.

data class UserProfile(
    val name: String,
    val nickname: String?
)

But importantly: nickname = null and “the "nickname" key was not present” are different data variants. Today we are not discussing how a specific serialization library will interpret these cases (that will come later, when we have a codec and settings), but as a contract designer you must understand the difference:

  • nickname: null often means “the value is known to be empty / removed / not set”
  • a missing field sometimes means “old data format,” “field not supported,” “we don’t send it”

For reading with your eyes, this means a simple rule: if you see null without quotes, it’s not a string; it’s exactly null. If you see "null", it’s a string of four letters n-u-l-l, and yes, it will behave like a string (and sometimes people accidentally break data this way).

6. JSON nesting: how it turns into nested Kotlin types

When JSON becomes “grown-up,” it almost always becomes nested. There’s no need to be afraid of it: nesting is just recursion of the same rules. An object can contain arrays, an array can contain objects, and so on. If you can confidently read the top level and know how to descend inside, you’re already 80% ready for real life.

Let’s make our Expense Tracker model a bit richer: let the category be not a string, but an object with a code and a title (purely to practice reading nesting).

The JSON for one expense will become:

{
  "id": 1,
  "title": "Coffee",
  "amount": 3.5,
  "category": { "code": "food", "title": "Food" }
}

And the Kotlin model becomes nested:

data class Category(
    val code: String,
    val title: String
)

data class Expense(
    val id: Int,
    val title: String,
    val amount: Double,
    val category: Category
)

Now we read the JSON with our eyes and literally “project” it into types:

  • top level { ... } → one Expense
  • field "category": { ... } → inside is an object → so the field type is Category, not String
  • inside category there are "code" and "title" → two string fields

If we now have a list of expenses, it will be an array of objects, each containing a nested object:

[
  {
    "id": 1,
    "title": "Coffee",
    "amount": 3.5,
    "category": { "code": "food", "title": "Food" }
  },
  {
    "id": 2,
    "title": "Bus",
    "amount": 2.0,
    "category": { "code": "transport", "title": "Transport" }
  }
]

In Kotlin, the goal is clear: List<Expense>, where inside Expense there is a Category.

And this is where a “pleasant insight” often happens: JSON nesting almost one-to-one matches the nesting of a data class. You literally get a “data tree,” and Kotlin types also become a tree.

7. How to read JSON with your eyes before writing code

When you first receive JSON (from a file, from a server, from a colleague, from logs—doesn’t matter), you want to start coding immediately. But it’s much cheaper to pause briefly and answer a couple of questions: what’s at the top level, what keys exist, where arrays are, where things are nullable. It’s like reading a recipe before turning on the oven. And yes, programmers can cook too—it’s just that our oven is called “prod.”

A very practical mini-algorithm is:

flowchart TD
    A["JSON as text"] --> B["trim()"]
    B --> C{"First character"}
    C -->|"{" | D["It's an object -> expect data class / Map"]
    C -->|"[" | E["It's an array -> expect List<...>"]
    C -->|"" | F["It's a string"]
    C -->|t/f| G["It's a boolean"]
    C -->|digit/minus| H["It's a number -> decide Int/Double"]
    C -->|n| I["It's null"]

Let’s implement a tiny helper function in our console project that at least hints “what’s on top” (this is not a parser; it’s just a quick diagnostic at the level of “look at the first character”).

fun detectTopLevelKind(jsonText: String): String {
    val first = jsonText.trim().firstOrNull() ?: return "empty"
    return when (first) {
        '{' -> "object"
        '[' -> "array"
        '"' -> "string"
        't', 'f' -> "boolean"
        'n' -> "null"
        '-', in '0'..'9' -> "number"
        else -> "unknown"
    }
}

fun main() {
    println(detectTopLevelKind(""" {"id": 1} """))             // object
    println(detectTopLevelKind(""" [1, 2, 3] """))             // array
    println(detectTopLevelKind("null"))                        // null
    println(detectTopLevelKind("  12.5 "))                     // number
}

Cheat sheet: JSON → Kotlin

When your brain is tired of curly braces, a table saves you. This is not a “law of the universe,” but a practical cheat sheet: which structures are most often chosen in Kotlin for the corresponding pieces of JSON. It’s important to remember: JSON itself doesn’t know what Int or Double is, so some mappings depend on the data contract.

JSON form Example Typical Kotlin form Comment
Object {}
{"id":1,"name":"Ada"}
data class User(...)
Most convenient path: a typed model
Object {}
{"a":1,"b":2}
Map<String, Int>
Suitable for dynamic keys
Array []
[1,2,3]
List<Int>
Order matters; elements are homogeneous by agreement
Array []
[{...},{...}]
List<Expense>
A common “list of entities” scenario
String
"hello"
String
Quotes are required
Number
3.5
Double
Fractional part → usually Double
Number
10
Int / Long
The choice depends on the range
Boolean
true
Boolean
Only true/false, without quotes
null
null
T?
Nullable type; otherwise Kotlin won’t allow it

8. Typical mistakes when working with JSON structure

Mistake #1: keys without quotes.
A very common story: someone writes something like { id: 1 }, because it looks like a JavaScript object in code. But JSON is stricter: the key must be a string in double quotes—"id": 1. If you forget the quotes, JSON stops being JSON, and the parser will honestly say: “I can’t eat that.”

Mistake #2: confusing a number and a string.
"1" and 1 look almost the same, but for the contract they are different types. If you accidentally put "3.5" as a string into the amount field, then a model expecting a number will “break” when reading. This is especially nasty when a human forms the data by hand (in a config or a test file).

Mistake #3: null and "null" are not the same thing.
null is a special value meaning “no value.” "null" is a regular string of four characters. If you mix them up, you can end up with a situation where instead of a missing value you suddenly have the text "null", and then weird checks start appearing in code like if (x == "null") (which is almost a sign the app is drifting into a sea of hacks).

Mistake #4: a trailing comma at the end of an object or array.
In some languages and formats, a “trailing comma” is allowed. In JSON it is not. That means [1,2,3,] and {"a":1,} are invalid. That’s exactly why people quickly arrive at the thought “I don’t want to assemble JSON by hand as strings.” And that’s a perfectly healthy thought.

Mistake #5: confusing the top level—object vs array.
If the top is {}, you expect “one object” (for example, Expense). If the top is [], you expect a “list of objects” (for example, List<Expense>). Trying to read an array as an object (or vice versa) is one of the most common reasons for errors at the beginning, because visually it can be easy to miss, especially when JSON is long and multi-line.

Mistake #6: built a Kotlin model but forgot that the nesting must match.
If in JSON the "category" field is an object {...}, but in Kotlin you left category: String, then even with perfect data the structural mapping won’t work. Nested {} almost always means a nested type (data class Category(…)) or, in a more dynamic approach, a nested Map. Holding onto a string where an object lives is a path to constant pain.

Mistake #7: expecting field order in an object to matter.
In a JSON object, keys matter, not order. So { "id": 1, "name": "Ada" } and { "name": "Ada", "id": 1 } are equivalent in meaning. If you catch yourself thinking “what if the parser reads it only if id is first,” that’s a signal that somewhere the concepts of an object {} and an array [] got mixed up, where order really does matter.

1
Task
Kotlin SELF, level 47, lesson 1
Locked
JSON Showcase
JSON Showcase
1
Task
Kotlin SELF, level 47, lesson 1
Locked
Quick Detector
Quick Detector
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION