Lektion 3.9 · Stufe 3 – Fortgeschritten

JSON mit Codable

ca. 15 Minuten Lesezeit

Lernziel

Nach dieser Lektion kannst du JSON lesen und verstehen, Swift-Typen mit Codable aus JSON erzeugen und wieder in JSON umwandeln, abweichende Namen mit CodingKeys zuordnen, fehlende Werte mit Optionals abfangen und Datumswerte richtig dekodieren.

Das Problem: Daten in einer fremden Sprache

Fast jede App bekommt Daten von außen: Wetterdaten, Rezepte, Nachrichten, Kontostände. Diese Daten kommen nicht als Swift-Structs an, sondern als Text in einem standardisierten Format. Das mit Abstand häufigste Format heißt JSON.

Bevor deine App mit den Daten arbeiten kann, müssen sie übersetzt werden: aus JSON-Text in deine eigenen Typen. Und manchmal auch umgekehrt, wenn du Daten an einen Server schickst oder speicherst.

In Swift erledigt das ein Protokoll namens Codable, und meist mit erstaunlich wenig Code.

Was ist JSON?

JSON steht für JavaScript Object Notation. Es ist ein Textformat, das Menschen lesen und Computer leicht verarbeiten können. So sieht ein Rezept als JSON aus:

JSON
{
    "id": 52772,
    "name": "Teriyaki Chicken",
    "category": "Chicken",
    "minutes": 35,
    "isVegetarian": false,
    "rating": 4.6,
    "tags": ["Asiatisch", "Schnell"],
    "author": {
        "name": "Anna",
        "country": "Österreich"
    }
}

JSON kennt nur wenige Bausteine, und du kennst alle aus Stufe 1:

JSONBeispielSwift
Objekt { … }{"name": "Anna"}Struct oder Dictionary
Array [ … ]["Asiatisch", "Schnell"]Array
Text"Teriyaki Chicken"String
Ganze Zahl35Int
Kommazahl4.6Double
Wahrheitswerttrue, falseBool
Nichtsnullnil (Optional)

Ein JSON-Objekt ist eine Sammlung von Schlüssel-Wert-Paaren, genau wie ein Dictionary (Lektion 1.7). Die Schlüssel stehen immer in Anführungszeichen.

Von JSON zu Swift: Decodable

Um dieses JSON in Swift zu verwenden, baust du eine passende Struct und lässt sie Codable erfüllen:

Swift
struct Author: Codable {
    let name: String
    let country: String
}

struct Recipe: Codable, Identifiable {
    let id: Int
    let name: String
    let category: String
    let minutes: Int
    let isVegetarian: Bool
    let rating: Double
    let tags: [String]
    let author: Author
}

Die Regel ist einfach: Jeder Schlüssel im JSON wird zu einer Eigenschaft mit genau demselben Namen und passendem Typ. Verschachtelte Objekte werden zu eigenen Structs, wie hier Author.

Dann übersetzt ein JSONDecoder den Text:

Swift
import Foundation

let json = """
{
    "id": 52772,
    "name": "Teriyaki Chicken",
    "category": "Chicken",
    "minutes": 35,
    "isVegetarian": false,
    "rating": 4.6,
    "tags": ["Asiatisch", "Schnell"],
    "author": { "name": "Anna", "country": "Österreich" }
}
"""

let data = Data(json.utf8)

do {
    let recipe = try JSONDecoder().decode(Recipe.self, from: data)
    print(recipe.name)          // Teriyaki Chicken
    print(recipe.author.name)   // Anna
    print(recipe.tags.count)    // 2
} catch {
    print("Fehler beim Lesen: \(error)")
}
  • Data(json.utf8) wandelt den Text in Bytes um. Daten aus dem Internet kommen ohnehin als Data an.
  • JSONDecoder().decode(Recipe.self, from: data) heißt: „Mach aus diesen Bytes ein Recipe.“ Recipe.self ist der Typ selbst (Lektion 3.3).
  • Die Funktion ist throws (Lektion 1.17): Passt das JSON nicht zur Struct, gibt es einen Fehler.

Tipp

Swift erzeugt den gesamten Übersetzungscode automatisch, weil alle Eigenschaften selbst Codable sind: String, Int, Double, Bool, Arrays davon und andere Codable-Structs. Genau wie bei Equatable (Lektion 1.15) schreibst du nur den Namen des Protokolls.

Arrays von Objekten

APIs liefern oft eine Liste:

JSON
[
    { "id": 1, "name": "Pizza" },
    { "id": 2, "name": "Pasta" }
]

Dann dekodierst du ein Array:

Swift
struct Dish: Codable {
    let id: Int
    let name: String
}

let dishes = try JSONDecoder().decode([Dish].self, from: data)

Verpackte Antworten

Sehr häufig steckt die eigentliche Liste in einem äußeren Objekt:

JSON
{
    "meals": [
        { "id": 1, "name": "Pizza" },
        { "id": 2, "name": "Pasta" }
    ]
}

Dann brauchst du eine kleine Struct für die Hülle:

Swift
struct MealResponse: Codable {
    let meals: [Dish]
}

let response = try JSONDecoder().decode(MealResponse.self, from: data)
let dishes = response.meals

Diese Art von Antwort wirst du in Projekt D sehen.

Nur, was du brauchst

Du musst nicht jedes Feld aus dem JSON übernehmen. Eigenschaften, die in deiner Struct fehlen, werden einfach ignoriert:

Swift
struct RecipeSummary: Codable {
    let id: Int
    let name: String
}

Diese Struct liest aus dem langen Rezept-JSON nur id und name. Alles andere wird übersprungen. Das ist praktisch, denn viele APIs liefern deutlich mehr, als eine App braucht.

Fehlende Werte: Optionals

Umgekehrt ist es kritisch: Erwartet deine Struct ein Feld, das im JSON fehlt oder null ist, schlägt das Dekodieren fehl. Bei Feldern, die nicht immer vorhanden sind, verwendest du deshalb ein Optional (Lektion 1.8):

Swift
struct Recipe: Codable {
    let id: Int
    let name: String
    let imageURL: String?     // nicht jedes Rezept hat ein Bild
    let videoURL: String?
}

Fehlt imageURL im JSON oder steht dort null, wird die Eigenschaft einfach nil.

Achtung

Die häufigste Fehlerquelle bei JSON: ein Feld als nicht-optional deklariert, das in manchen Datensätzen fehlt. Mit Testdaten funktioniert alles, bei echten Daten stürzt das Laden ab. Lies die Dokumentation der API genau und mach im Zweifel Felder optional.

Andere Namen: CodingKeys

JSON-Schlüssel folgen nicht immer den Swift-Konventionen. Oft heißen sie image_url mit Unterstrich, strMeal mit seltsamen Vorsilben oder einfach anders, als du es willst. Mit CodingKeys ordnest du sie zu:

JSON
{ "idMeal": "52772", "strMeal": "Teriyaki Chicken", "strMealThumb": "https://…" }
Swift
struct Meal: Codable, Identifiable {
    let id: String
    let name: String
    let thumbnail: String?

    enum CodingKeys: String, CodingKey {
        case id = "idMeal"
        case name = "strMeal"
        case thumbnail = "strMealThumb"
    }
}

CodingKeys ist ein Enum (Lektion 1.14) mit String-Raw-Values: Der Fall ist der Name in Swift, der Raw Value der Name im JSON. Sobald du CodingKeys angibst, musst du alle Eigenschaften aufführen, die dekodiert werden sollen.

Unterstriche automatisch umwandeln

Verwendet die API durchgehend Unterstriche (first_name, created_at), gibt es eine Abkürzung:

Swift
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase

Dann wird first_name automatisch zu firstName, ganz ohne CodingKeys. Diese Schreibweise mit Unterstrichen heißt snake_case.

Datumswerte

JSON hat keinen eigenen Datumstyp. Daten kommen meist als Text im Format ISO 8601 an:

JSON
{ "title": "Treffen", "date": "2026-10-20T16:00:00Z" }

Damit JSONDecoder daraus ein Date macht, sagst du ihm, welches Format er erwarten soll:

Swift
struct Event: Codable {
    let title: String
    let date: Date
}

let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let event = try decoder.decode(Event.self, from: data)

Von Swift zu JSON: Encodable

Codable funktioniert in beide Richtungen. Mit JSONEncoder wird aus deiner Struct wieder JSON:

Swift
let dish = Dish(id: 3, name: "Kaiserschmarrn")

let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted

let data = try encoder.encode(dish)
print(String(decoding: data, as: UTF8.self))

Ausgabe:

JSON
{
  "id" : 3,
  "name" : "Kaiserschmarrn"
}

.prettyPrinted sorgt für Einrückungen. Ohne steht alles in einer Zeile, was kürzer, aber schwerer lesbar ist.

Das brauchst du, wenn du Daten an einen Server schickst oder, wie in Lektion 3.13, Daten auf dem Gerät speicherst.

Tipp

Codable ist eigentlich eine Abkürzung für zwei Protokolle: Decodable (aus JSON lesen) und Encodable (in JSON schreiben). Brauchst du nur eine Richtung, reicht das jeweilige Protokoll.

Fehler beim Dekodieren verstehen

Passt das JSON nicht zur Struct, ist die Fehlermeldung zunächst unübersichtlich. Mit einem genaueren catch wird sie hilfreich:

Swift
do {
    let recipe = try JSONDecoder().decode(Recipe.self, from: data)
} catch DecodingError.keyNotFound(let key, _) {
    print("Fehlendes Feld: \(key.stringValue)")
} catch DecodingError.typeMismatch(let type, let context) {
    print("Falscher Typ \(type) bei \(context.codingPath.map(\.stringValue))")
} catch {
    print("Anderer Fehler: \(error)")
}
FehlerBedeutungTypische Lösung
keyNotFoundEin Feld fehlt im JSONEigenschaft optional machen oder CodingKeys prüfen
typeMismatchFalscher Typ, z. B. "35" statt 35Typ der Eigenschaft anpassen
valueNotFoundFeld ist null, aber nicht optionalEigenschaft optional machen
dataCorruptedKein gültiges JSON oder falsches DatumsformatJSON und dateDecodingStrategy prüfen

Typische Fehler

Zahl als Text

Manche APIs liefern Zahlen in Anführungszeichen: "id": "52772". Dann muss die Eigenschaft ein String sein, kein Int. Sonst gibt es typeMismatch.

Tippfehler im Namen

let catgory: String findet im JSON kein catgory und wirft keyNotFound. Namen müssen exakt übereinstimmen, inklusive Groß- und Kleinschreibung.

CodingKeys unvollständig

Sobald es CodingKeys gibt, werden nur die dort aufgeführten Eigenschaften dekodiert. Fehlt eine Eigenschaft im Enum, die keinen Standardwert hat:

Fehlermeldung

Type 'Meal' does not conform to protocol 'Decodable'

let id = UUID() und Codable

Swift
struct Item: Codable {
    let id = UUID()
    let name: String
}

Achtung

Immutable property will not be decoded because it is declared with an initial value which cannot be overwritten

Die id wird beim Dekodieren immer neu erzeugt und nicht aus dem JSON gelesen. Kommt die id aus dem JSON, deklariere sie ohne Startwert: let id: Int.

FAQ

Anfänger

Was bedeutet das überhaupt?

Wie finde ich heraus, wie das JSON einer API aussieht? Öffne die Adresse der API im Browser. Viele Browser zeigen JSON übersichtlich formatiert an. Außerdem steht es meist in der Dokumentation der API.

Muss ich jede Struct von Hand schreiben? Ja, aber es ist schnell gemacht: Schlüssel für Schlüssel abschreiben und den passenden Typ wählen. Es gibt auch Werkzeuge, die aus JSON-Beispielen Structs erzeugen. Prüf deren Ergebnis aber immer, vor allem bei Optionals.

Ist JSON das einzige Format? Nein, aber das mit Abstand häufigste. Mit PropertyListEncoder gibt es zum Beispiel auch Apples eigenes Format, das intern für Einstellungen verwendet wird.

Fortgeschritten

Warum verhält es sich so?

Wie lese ich ein Feld, das mal Zahl, mal Text ist? Mit einem eigenen init(from decoder: Decoder). Darin versuchst du zuerst try? container.decode(Int.self, forKey: .id) und, falls das scheitert, die Text-Variante. Solche Sonderfälle sind lästig, aber manchmal nötig.

Wie flache ich verschachteltes JSON in eine einfache Struct ab? Ebenfalls mit einem eigenen init(from:) und nestedContainer(keyedBy:forKey:). Oft ist es aber einfacher, die verschachtelte Struktur 1:1 nachzubauen und eine berechnete Eigenschaft für den bequemen Zugriff zu ergänzen.

Profi

Was passiert unter der Haube?

Wie funktioniert die automatische Erzeugung? Der Compiler erzeugt für Codable-Typen, deren Eigenschaften alle selbst Codable sind, ein verstecktes CodingKeys-Enum, einen init(from: Decoder) und eine Methode encode(to: Encoder). Sie lesen bzw. schreiben jede Eigenschaft über einen Keyed Container. Schreibst du eines davon selbst, verwendet Swift deine Version und erzeugt nur die fehlenden Teile.

Ist Codable an JSON gebunden? Nein. Codable beschreibt nur, welche Werte ein Typ hat. Das Format bestimmt der Encoder bzw. Decoder: JSON, Property Lists oder eigene Formate. Dieselbe Struct kann mit verschiedenen Encodern in verschiedene Formate geschrieben werden.

Übungen

Anfänger

Übung 1: Wetter dekodieren

Dekodiere im Playground dieses JSON in eine passende Struct und gib „Wien: 18.5 °C, sonnig“ aus:

JSON
{ "city": "Wien", "temperature": 18.5, "condition": "sonnig" }
Lösung Übung 1Erst selbst probieren!
Swift
import Foundation

struct Weather: Codable {
    let city: String
    let temperature: Double
    let condition: String
}

let json = """
{ "city": "Wien", "temperature": 18.5, "condition": "sonnig" }
"""

do {
    let weather = try JSONDecoder().decode(Weather.self, from: Data(json.utf8))
    print("\(weather.city): \(weather.temperature) °C, \(weather.condition)")
} catch {
    print(error)
}
Fortgeschritten

Übung 2: Seltsame Namen

Dekodiere dieses JSON in eine Struct mit den Eigenschaften id, title, price und isOnSale:

JSON
{ "products": [
    { "product_id": 1, "product_title": "Kaffee", "price_eur": 4.5, "on_sale": true },
    { "product_id": 2, "product_title": "Tee", "price_eur": 3.2, "on_sale": false }
] }
Lösung Übung 2Erst selbst probieren!
Swift
struct Product: Codable {
    let id: Int
    let title: String
    let price: Double
    let isOnSale: Bool

    enum CodingKeys: String, CodingKey {
        case id = "product_id"
        case title = "product_title"
        case price = "price_eur"
        case isOnSale = "on_sale"
    }
}

struct ProductResponse: Codable {
    let products: [Product]
}

let response = try JSONDecoder().decode(ProductResponse.self, from: Data(json.utf8))
for product in response.products {
    print(product.title, product.price, product.isOnSale)
}
Profi

Übung 3: Hin und zurück

Leg ein Array mit drei Gewohnheiten an (eine Codable-Version der Habit-Struct aus Projekt C mit id: UUID, Name und completions: [Date]). Kodiere es mit .iso8601 und .prettyPrinted zu JSON, gib es aus, dekodiere es wieder und prüf mit ==, ob das Ergebnis dem Original entspricht. Dafür muss Habit auch Equatable sein.

Zusammenfassung

  • JSON ist das Standardformat für Daten aus dem Internet: Objekte, Arrays, Texte, Zahlen, Bools und null.
  • Codable übersetzt automatisch zwischen JSON und Swift-Typen, wenn die Namen übereinstimmen.
  • JSONDecoder().decode(Typ.self, from: data) liest, JSONEncoder().encode(wert) schreibt.
  • Felder, die fehlen können, werden Optionals.
  • CodingKeys ordnet abweichende Namen zu, .convertFromSnakeCase wandelt Unterstriche automatisch um.
  • .iso8601 dekodiert Datumswerte im Standardformat.
  • DecodingError verrät, welches Feld nicht passt.

Spickzettel

Swift
struct Meal: Codable, Identifiable {
    let id: String
    let name: String
    let thumbnail: String?
    enum CodingKeys: String, CodingKey {
        case id = "idMeal", name = "strMeal", thumbnail = "strMealThumb"
    }
}
struct MealResponse: Codable { let meals: [Meal]? }

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
decoder.dateDecodingStrategy = .iso8601
let response = try decoder.decode(MealResponse.self, from: data)

let data = try JSONEncoder().encode(meal)

Lektion geschafft?

Markiere sie als erledigt, damit du deinen Fortschritt siehst.