Android Coden
Android 8 min lesen

JSON-Grundlagen

JSON ist das Standardformat für App-Daten. Du lernst Objekte, Arrays und Serialisierung im Android-Kontext.

JSON ist das Textformat, das dir in Android-Apps ständig begegnet, sobald Daten über eine API, aus einer lokalen Datei oder aus einem Cache gelesen werden. Wenn du verstehst, wie JSON-Objekte, Arrays und Serialisierung zusammenhängen, kannst du Serverantworten sauber in Kotlin-Modelle übersetzen und Fehler früher erkennen.

Was ist das?

JSON steht für JavaScript Object Notation, ist aber längst nicht mehr an JavaScript gebunden. Für dich als Android-Entwickler ist JSON vor allem ein einfach lesbares Datenformat. Es beschreibt Werte als Text: Zeichenketten, Zahlen, Wahrheitswerte, null, Objekte und Arrays. Ein Objekt besteht aus benannten Feldern. Ein Array ist eine geordnete Liste von Werten.

Ein typisches JSON-Objekt kann so aussehen:

{
  "id": 42,
  "title": "Kotlin lernen",
  "completed": false
}

Hier ist das gesamte Dokument ein Objekt. Es enthält die Felder id, title und completed. Das Feld id ist eine Zahl, title ist Text und completed ist ein Boolean. Ein Array würdest du nutzen, wenn mehrere Einträge übertragen werden:

[
  {
    "id": 42,
    "title": "Kotlin lernen",
    "completed": false
  },
  {
    "id": 43,
    "title": "JSON verstehen",
    "completed": true
  }
]

Das mentale Modell ist wichtig: JSON ist nicht deine App-Architektur und auch nicht dein UI-State. JSON ist ein Austauschformat. Es liegt zwischen deiner App und einer Datenquelle, zum Beispiel einem Backend. Deine App sollte JSON nicht überall herumreichen, sondern es an einer klaren Stelle in Kotlin-Datenklassen umwandeln. Diese Umwandlung nennt man Serialisierung, wenn Kotlin-Daten zu JSON werden, und Deserialisierung, wenn JSON zu Kotlin-Objekten wird. In der Praxis wird der Begriff Serialisierung oft für beide Richtungen verwendet.

Im Android-Kontext sitzt JSON meist in der Data Layer. Dort liegen Repositorys, Netzwerkquellen, lokale Datenquellen und Mapper. Eine Compose-Oberfläche sollte nicht wissen müssen, ob ein Aufgaben-Titel aus JSON, Room, DataStore oder einem Test-Dummy kommt. Sie bekommt idealerweise ein stabiles UI-Modell oder einen Screen-State. Dadurch bleibt dein Code testbarer und du kannst Datenquellen austauschen, ohne die Oberfläche neu zu bauen.

Wie funktioniert es?

JSON folgt wenigen Regeln. Feldnamen stehen in doppelten Anführungszeichen. Strings ebenfalls. Objekte werden mit {} geschrieben, Arrays mit []. Zwischen Feldnamen und Wert steht ein Doppelpunkt. Mehrere Felder oder Array-Elemente werden mit Kommas getrennt. Diese einfache Syntax ist einer der Gründe, warum JSON in APIs so verbreitet ist.

Für Kotlin ist JSON aber zunächst nur Text. Kotlin weiß nicht automatisch, dass "id": 42 zu einer Eigenschaft id: Long in einer Datenklasse gehört. Dafür brauchst du eine Bibliothek, zum Beispiel Kotlin Serialization, Moshi oder Gson. Moderne Android-Projekte verwenden häufig Kotlin Serialization oder Moshi, weil sie gut zu Kotlin-Datenklassen und Nullability passen. Das Prinzip bleibt gleich: Du beschreibst ein Modell, und die Bibliothek übersetzt zwischen JSON-Feldern und Kotlin-Eigenschaften.

Beispiel für ein Kotlin-Modell:

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class TaskDto(
    val id: Long,
    val title: String,
    @SerialName("completed")
    val isCompleted: Boolean
)

Das Suffix Dto steht hier für Data Transfer Object. Es macht sichtbar, dass diese Klasse die Form der übertragenen Daten beschreibt. Sie ist nicht zwingend dasselbe wie dein Domain-Modell oder dein UI-Modell. Diese Trennung wirkt am Anfang vielleicht streng, hilft aber, wenn das Backend Feldnamen ändert, zusätzliche Werte liefert oder technische Details enthält, die deine Oberfläche nicht braucht.

Arrays werden in Kotlin meistens als List<T> abgebildet:

@Serializable
data class TaskListResponse(
    val tasks: List<TaskDto>
)

Wenn die Serverantwort direkt ein JSON-Array ist, kann auch eine Liste ohne umschließendes Objekt deserialisiert werden. Trotzdem liefern viele APIs ein Objekt mit einem Array-Feld zurück, weil dort später weitere Informationen ergänzt werden können, etwa nextPage, totalCount oder updatedAt.

Ein wichtiger Punkt ist Nullability. JSON kann Felder auslassen oder den Wert null liefern. Kotlin unterscheidet aber klar zwischen String und String?. Wenn dein Modell val title: String erwartet, das JSON-Feld aber fehlt, kann die Deserialisierung fehlschlagen. Das ist gut, wenn title für deine App wirklich Pflicht ist. Es ist problematisch, wenn du die Datenquelle nicht kontrollierst oder ältere API-Versionen noch andere Antworten senden. Dann brauchst du Default-Werte oder nullable Eigenschaften:

@Serializable
data class UserDto(
    val id: Long,
    val displayName: String = "Unbekannt",
    val avatarUrl: String? = null
)

So legst du bewusst fest, welche Daten Pflicht sind und welche fehlen dürfen. Diese Entscheidung gehört nicht zufällig in den Code, sondern sollte fachlich begründet sein.

In einer sauberen Android-Architektur wird JSON am Rand des Systems verarbeitet. Ein Retrofit-Service oder eine andere Netzwerkkomponente lädt Text oder direkt typisierte DTOs. Ein Repository ruft diese Quelle auf, behandelt Fehler und mappt DTOs in Modelle, mit denen der Rest der App arbeitet. Bei Offline-First-Ansätzen wird die Antwort zusätzlich in eine lokale Datenquelle geschrieben, zum Beispiel in eine Datenbank. Auch dort solltest du nicht blind JSON als String speichern, wenn du eigentlich einzelne Felder abfragen, sortieren oder synchronisieren musst.

In der Praxis

Stell dir eine App vor, die Aufgaben von einem Server lädt. Die API liefert diese Antwort:

{
  "tasks": [
    {
      "id": 1,
      "title": "Projekt öffnen",
      "completed": true
    },
    {
      "id": 2,
      "title": "JSON-Modell prüfen",
      "completed": false
    }
  ]
}

Dafür kannst du DTOs definieren und anschließend in ein Domain-Modell mappen:

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class TasksResponseDto(
    val tasks: List<TaskDto>
)

@Serializable
data class TaskDto(
    val id: Long,
    val title: String,
    @SerialName("completed")
    val isCompleted: Boolean
)

data class Task(
    val id: Long,
    val title: String,
    val done: Boolean
)

fun TaskDto.toDomain(): Task {
    return Task(
        id = id,
        title = title.trim(),
        done = isCompleted
    )
}

fun TasksResponseDto.toDomain(): List<Task> {
    return tasks.map { it.toDomain() }
}

Das Beispiel zeigt eine nützliche Regel: Lasse die Netzwerkform nicht unkontrolliert in deine App hineinlaufen. TaskDto beschreibt, was die API sendet. Task beschreibt, was deine App fachlich braucht. Der Mapper ist der Ort, an dem du kleine Anpassungen vornimmst, etwa title.trim(). Du kannst dort auch fehlerhafte Werte abfangen oder bewusst ablehnen.

In einem Repository kann das dann so aussehen:

class TaskRepository(
    private val remoteDataSource: TaskRemoteDataSource
) {
    suspend fun loadTasks(): List<Task> {
        val response = remoteDataSource.fetchTasks()
        return response.toDomain()
    }
}

interface TaskRemoteDataSource {
    suspend fun fetchTasks(): TasksResponseDto
}

Für eine Compose-Oberfläche ist danach nicht mehr wichtig, dass die Daten aus JSON kamen. Ein ViewModel kann List<Task> in einen Screen-State überführen und die UI rendert diesen Zustand. Genau diese Trennung macht den Code leichter testbar: Du kannst den Mapper separat testen, das Repository mit einer Fake-Datenquelle prüfen und die UI mit fertigen Zuständen anzeigen.

Eine typische Stolperfalle ist das Verwechseln von Objekt und Array. Wenn die API ein Objekt liefert, dein Code aber eine Liste erwartet, schlägt die Deserialisierung fehl. Der Fehler wirkt für Anfänger oft kryptisch, ist aber meist eine Strukturfrage. Vergleiche dann das echte JSON mit deinem Kotlin-Modell: Beginnt die Antwort mit {, erwartest du ein Objekt. Beginnt sie mit [, erwartest du ein Array. Prüfe außerdem die Feldnamen exakt. is_completed, isCompleted und completed sind drei verschiedene Namen, solange du sie nicht über Annotationen oder Konfigurationen zuordnest.

Eine zweite Stolperfalle sind Zahlen. JSON kennt zwar Zahlen, aber nicht deine Kotlin-Typen Int, Long, Float oder Double. Wenn IDs sehr groß werden, kann Int zu klein sein. Für technische IDs ist Long oft die robustere Wahl. Für Geldbeträge solltest du nicht unüberlegt Double nehmen, weil Rundungsfehler entstehen können. Das ist kein JSON-Spezialproblem, aber JSON macht solche Annahmen sichtbar.

Eine dritte Stolperfalle betrifft Versionierung. Backends entwickeln sich weiter. Neue Felder sind meist unkritisch, wenn deine Bibliothek unbekannte Schlüssel ignoriert oder entsprechend konfiguriert ist. Fehlende oder umbenannte Pflichtfelder sind kritischer. Deshalb lohnt es sich, Beispielantworten aus der API-Dokumentation oder aus aufgezeichneten Testdaten in Unit-Tests zu verwenden:

import kotlinx.serialization.json.Json
import kotlin.test.Test
import kotlin.test.assertEquals

class TaskJsonTest {
    private val json = Json {
        ignoreUnknownKeys = true
    }

    @Test
    fun parsesTaskResponse() {
        val raw = """
            {
              "tasks": [
                { "id": 1, "title": "Projekt öffnen", "completed": true }
              ]
            }
        """.trimIndent()

        val dto = json.decodeFromString<TasksResponseDto>(raw)
        val tasks = dto.toDomain()

        assertEquals(1, tasks.size)
        assertEquals("Projekt öffnen", tasks.first().title)
        assertEquals(true, tasks.first().done)
    }
}

Solche Tests sind klein, aber wertvoll. Sie prüfen nicht nur, ob die Bibliothek grundsätzlich funktioniert. Sie sichern deine Annahmen über die Form der API-Antwort. In Code-Reviews solltest du besonders auf drei Fragen achten: Sind Pflichtfelder wirklich Pflicht? Werden externe DTOs von internen Modellen getrennt? Gibt es einen Test für mindestens eine echte oder realistische JSON-Antwort?

Für den Alltag kannst du dir eine einfache Entscheidungsregel merken: JSON gehört an Systemgrenzen. Dort darfst du es parsen, validieren und mappen. Innerhalb deiner App arbeitest du mit klaren Kotlin-Typen. Dadurch nutzt du Kotlin so, wie es gedacht ist: mit Nullability, Datenklassen, Typsicherheit und lesbaren Funktionen. Das reduziert Fehler, die sonst erst zur Laufzeit auffallen würden.

Fazit

JSON-Grundlagen sind kein Nebenthema, sondern tägliches Handwerk in Android-Projekten mit Netzwerkdaten, Caches und Offline-First-Strategien. Übe gezielt, indem du eine kleine Beispielantwort in DTOs übersetzt, einen Mapper schreibst und einen Unit-Test für die Deserialisierung ergänzt. Prüfe im Debugger, welche Werte wirklich ankommen, und achte im Code-Review darauf, dass JSON nicht bis in die Compose-Oberfläche durchsickert. So baust du ein stabiles Verständnis dafür auf, wie Daten von außen kontrolliert in deine Kotlin-App gelangen.

Quellen (4)
Redaktion

Geschrieben von

Redaktion

Das Redaktionsteam recherchiert und schreibt Artikel zu aktuellen Themen rund um Tech, Lifestyle und Ratgeber.