Damit ein Swift-Codable-Modell mit älteren Darstellungen im Speicher kompatibel bleibt, solltest du tatsächlich fehlende Werte optional machen und Dekodierungs-Fallbacks explizit implementieren. Ein Standardwert in der Deklaration allein sorgt nicht dafür, dass ein fehlendes codiertes Feld erfolgreich dekodiert wird.

Diese Anleitung richtet sich an Mac-Entwickler, die ein einfaches Desktop-Layoutmodell aktualisieren. Die konkrete Aufgabe besteht darin, das überarbeitete Modell ältere synthetische Darstellungen akzeptieren zu lassen, dabei bereitgestellte Werte zu erhalten und inkompatible Typen zu melden. Jedes Beispiel erstellt und verarbeitet Werte im Speicher.

Was ändert sich, wenn ein Feld optional wird oder einen Standardwert erhält?

Codable kombiniert Codierung und Dekodierung. Swift kann diese Implementierungen synthetisieren, wenn die gespeicherten Eigenschaften die erforderlichen Protokolle unterstützen. Dadurch entfällt routinemäßiger Code, doch eine Änderung, die weiterhin kompiliert, kann beeinflussen, welche Darstellungen das Modell akzeptiert. Apple dokumentiert sowohl die automatische Konformität als auch benutzerdefinierte Implementierungen unter Benutzerdefinierte Typen codieren und dekodieren.

Betrachte ein Layout-Kachelmodell mit einem erforderlichen Titel und Untertitel. Nun soll der Untertitel optional sein und eine neue Spaltenanzahl standardmäßig drei betragen. Das sind zwei getrennte Entscheidungen: Der Untertitel darf keinen Wert haben, während die Spaltenanzahl nach der Dekodierung immer einen brauchbaren Wert haben muss.

Bei gewöhnlichen veränderlichen Eigenschaften, die in die synthetisierte Dekodierung einbezogen werden, behandelt Swift eine optionale Eigenschaft so, dass sowohl ein fehlender Schlüssel als auch ein Nullwert zulässig ist. Ein Initialisierer in der Deklaration ist kein Dekodierungs-Fallback: Eine optionale Eigenschaft, die mit einem Wert ungleich nil initialisiert wird, kann dennoch als nil dekodiert werden, und eine nicht optionale Eigenschaft mit einem Standardwert in der Deklaration kann weiterhin fehlschlagen, wenn ihr Schlüssel fehlt. Die Erläuterung des Swift-Teams zur Codable-Synthese beschreibt dieses beabsichtigte Verhalten.

Die folgende Entscheidungstabelle übersetzt diese Regeln in eine Feldrichtlinie. Sie setzt ein ganzzahliges Feld und einen vorhandenen Wert voraus, der sich in Int umwandeln lässt. Für die letzten beiden Zeilen ist eine benutzerdefinierte Dekodierung erforderlich.

Feldrichtlinie Schlüssel fehlt Expliziter Nullwert Gültige Ganzzahl Inkompatibler Typ
Synthetisiertes var columns: Int Fehler Fehler Wert verwenden Fehler
Synthetisiertes var columns: Int = 3 Fehler Fehler Wert verwenden Fehler
Synthetisiertes var columns: Int? nil nil Wert verwenden Fehler
Benutzerdefinierter Fallback bei Fehlen und Nullwert 3 3 Wert verwenden Fehler
Benutzerdefinierter Fallback nur bei Fehlen 3 Fehler Wert verwenden Fehler

Der Unterschied ergibt sich aus den Methoden des Containers. decode(_:forKey:) erfordert einen Eintrag und einen nicht-nullwertigen, konvertierbaren Wert. decodeIfPresent(_:forKey:) gibt bei fehlendem Schlüssel oder Nullwert nil zurück, löst bei einem inkompatiblen Typ jedoch weiterhin einen Fehler aus. ?? 3 auf das Ergebnis der zweiten Methode angewendet liefert den Fallback.

Die Kompatibilitätsrichtlinie im Speicher implementieren und prüfen

Verwende dieses Beispiel in einer Swift-Ausführungsumgebung auf deinem Mac, in der Foundation verfügbar ist und Anweisungen auf oberster Ebene unterstützt werden, etwa in einem bestehenden Playground. Führe den gesamten Block zusammen aus. Foundations JSONEncoder erstellt eine Data-Darstellung, und JSONDecoder erzeugt daraus einen neuen typisierten Wert. Das Beispiel verwendet weder ein wörtliches codiertes Dokument noch externe Eingaben.

Die gewählte Richtlinie ist explizit: title bleibt erforderlich; ein fehlender oder nullwertiger subtitle wird zu nil; ein fehlender oder nullwertiger columns wird zu drei. Ein vorhandener, inkompatibler Spaltenwert bleibt ein Fehler. Der benutzerdefinierte Initialisierer übernimmt die Dekodierung, während die Codierung gemäß den CodingKeys des Modells weiterhin synthetisiert wird. Apple dokumentiert diesen Anpassungsansatz unter Benutzerdefinierte Typen codieren und dekodieren.

Dies ist ein vollständiges Programm mit synthetischen Testdaten und Prüfungen. Swift war in der Rechercheumgebung nicht verfügbar. Daher wurde das Beispiel anhand der dokumentierten APIs geprüft, aber nicht ausgeführt.

import Foundation

struct LegacyTile: Codable {
    var title: String
    var subtitle: String
}

struct Tile: Codable, Equatable {
    static let defaultColumns = 3

    var title: String
    var subtitle: String?
    var columns: Int = Tile.defaultColumns

    enum CodingKeys: String, CodingKey {
        case title, subtitle, columns
    }
}

extension Tile {
    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        title = try values.decode(String.self, forKey: .title)
        subtitle = try values.decodeIfPresent(String.self, forKey: .subtitle)
        columns = try values.decodeIfPresent(Int.self, forKey: .columns)
            ?? Self.defaultColumns
    }
}

struct MinimalTile: Encodable {
    var title: String
}

struct NullTile: Encodable {
    enum CodingKeys: String, CodingKey {
        case title, subtitle, columns
    }

    func encode(to encoder: Encoder) throws {
        var values = encoder.container(keyedBy: CodingKeys.self)
        try values.encode("Circle", forKey: .title)
        try values.encodeNil(forKey: .subtitle)
        try values.encodeNil(forKey: .columns)
    }
}

struct WrongColumnsTile: Encodable {
    var title: String
    var columns: String
}

let encoder = JSONEncoder()
let decoder = JSONDecoder()

// An older representation keeps its subtitle and gains the fallback.
let legacyBytes = try encoder.encode(
    LegacyTile(title: "Circle", subtitle: "Blue")
)
let upgraded = try decoder.decode(Tile.self, from: legacyBytes)
precondition(upgraded.title == "Circle")
precondition(upgraded.subtitle == "Blue")
precondition(upgraded.columns == 3)

// Missing keys and explicit nulls follow the chosen policy.
let minimalBytes = try encoder.encode(MinimalTile(title: "Circle"))
let minimal = try decoder.decode(Tile.self, from: minimalBytes)
precondition(minimal.subtitle == nil)
precondition(minimal.columns == 3)

let nullBytes = try encoder.encode(NullTile())
let explicitNull = try decoder.decode(Tile.self, from: nullBytes)
precondition(explicitNull == minimal)

// A supplied nondefault value survives an encode/decode cycle.
let supplied = Tile(title: "Square", subtitle: "Green", columns: 5)
let suppliedBytes = try encoder.encode(supplied)
let restored = try decoder.decode(Tile.self, from: suppliedBytes)
precondition(restored == supplied)

// Incompatible types must remain visible as decoding failures.
let wrongBytes = try encoder.encode(
    WrongColumnsTile(title: "Circle", columns: "wide")
)
do {
    _ = try decoder.decode(Tile.self, from: wrongBytes)
    preconditionFailure("Expected a type mismatch")
} catch DecodingError.typeMismatch(_, let context) {
    precondition(context.codingPath.last?.stringValue == "columns")
}

// A newer representation may no longer satisfy an older reader.
let currentBytes = try encoder.encode(minimal)
do {
    _ = try decoder.decode(LegacyTile.self, from: currentBytes)
    preconditionFailure("Expected the required subtitle to be missing")
} catch DecodingError.keyNotFound(let key, _) {
    precondition(key.stringValue == "subtitle")
}

print("All in-memory compatibility checks passed.")

Die erwartete Abschlussausgabe lautet Alle Kompatibilitätsprüfungen im Speicher bestanden. Unerwartete Fehler werden weitergegeben, statt in Erfolg umgewandelt zu werden. Die Testdaten decken bewusst unterschiedliche Darstellungen ab: MinimalTile lässt Felder aus, während NullTile mit encodeNil(forKey:) explizite Null-Einträge erstellt.

init(from:) in einer Erweiterung zu belassen, erhält auch den Memberwise-Initialisierer der Struktur, sodass das Beispiel Tile(title:subtitle:columns:) erstellen kann. Der benutzerdefinierte Dekodierungsinitialisierer weist jeder gespeicherten Instanzeigenschaft einen Wert zu, sodass das Ergebnis vor seiner Rückgabe vollständig initialisiert ist.

Soll Null den Standardwert verwenden oder weiterhin ein Fehler sein?

Entscheide nach der Bedeutung des Feldes. Bei diesem beispielhaften Layout ist es eine sinnvolle Richtlinie, sowohl bei Fehlen als auch bei Nullwert die standardmäßige Spaltenanzahl zu verwenden. Die Entscheidung liegt dennoch bei der Anwendung: decodeIfPresent fasst diese beiden Zustände bewusst zusammen, und der Fallback führt dann zum gleichen Ergebnis. Das dokumentierte Rückgabeverhalten macht diese Folge deutlich.

Wenn „Layoutstandard verwenden“ bei Fehlen gilt, ein bereitgestellter Nullwert aber fehlschlagen soll, ersetze innerhalb des Programms nur die Zuweisung columns in init(from:) durch diesen Ausschnitt. Alle anderen Voraussetzungen und Definitionen bleiben unverändert.

// Replacement fragment inside Tile.init(from:).
if values.contains(.columns) {
    columns = try values.decode(Int.self, forKey: .columns)
} else {
    columns = Self.defaultColumns
}

contains(_:) gibt an, ob ein Eintrag vorhanden ist, auch wenn es sich um einen Null-Eintrag handelt. Deshalb nimmt ein expliziter Nullwert den Zweig decode und führt zu valueNotFound; nur bei Fehlen wird der Standardwert verwendet. Bei dieser überarbeiteten Richtlinie muss die Prüfung des Null-Testfalls im ursprünglichen Programm so geändert werden, dass sie diesen Fehler statt der Gleichheit mit minimal erwartet.

Ändere die Zuweisung nicht zu (try? values.decode(Int.self, forKey: .columns)) ?? 3, es sei denn, tatsächlich jeder Dekodierungsfehler soll verworfen werden. Dieser Ausdruck ersetzt auch einen inkompatiblen bereitgestellten Wert durch drei. Mit try decodeIfPresent bleibt der Unterschied zwischen einem akzeptierten fehlenden Wert und einer Darstellung erhalten, die das Feld nicht interpretieren kann, wie im Fehlervertrag der Dekodierungsmethode festgelegt.

Funktioniert die Kompatibilität in beide Richtungen?

Prüfe jeden Leser einzeln. Das Programm zeigt, dass der überarbeitete Leser die ältere Darstellung akzeptiert. Es zeigt auch eine Einschränkung: Der ältere Leser benötigt weiterhin subtitle und weist daher eine neuere Darstellung zurück, in der dieses Feld fehlt. Die Änderung der neuen Swift-Eigenschaft zu optional ändert die Anforderungen des alten Typs nicht.

Bei synthetisierter Codierung lässt die bedingte Codierung optionale nil-Werte aus. Swifts encodeIfPresent(_:forKey:) dokumentiert, dass nur Werte ungleich nil codiert werden. Einen expliziten Nullwert zu codieren ist mit encodeNil möglich, erfüllt aber weiterhin nicht die Anforderungen einer älteren nicht optionalen String-Eigenschaft, die einen Wert voraussetzt.

Wenn ein älterer Leser weiterhin verwendbar bleiben muss, definiere eine explizite Darstellung, die seine Anforderungen erfüllt. In diesem synthetischen Beispiel könnte ein Adapter vor der Codierung ein LegacyTile mit einem festgelegten Untertitel erstellen. Ob ein fehlender Untertitel in eine bestimmte Zeichenfolge umgewandelt werden soll, ist eine separate Produktentscheidung. Erfinde diese Bedeutung nicht stillschweigend in einem allgemeinen Fallback.

Unterscheide außerdem zwischen der Erhaltung von Werten und der Erhaltung der Darstellung. Im Beispiel werden sowohl ein fehlender Untertitel als auch ein expliziter Nullwert zum selben Swift-Wert nil. Das Modell kann daher nicht wiedergeben, welche dieser beiden Eingaben es erhalten hat. Ebenso ergibt die Dekodierung einer fehlenden Spaltenanzahl als drei und die anschließende erneute Codierung des Modells eine konkrete Spaltenanzahl. Dies sind direkte Folgen des gewählten Modells und der Dekodierungsrichtlinie.

Was solltest du prüfen, bevor du das Modell änderst?

Verwende bei jeder Feldänderung eine kleine Kompatibilitätsmatrix. Beginne mit einer älteren Darstellung und variiere dann jeweils ein Feld. So kann ein erfolgreicher Roundtrip keinen Fehler verbergen, der nur bei einem fehlenden Schlüssel auftritt.

  • Ältere Eingabe: Bestätige, dass der überarbeitete Leser sie akzeptiert und vorhandene Werte erhält.
  • Fehlendes Feld: Bestätige, dass das Ergebnis dem beabsichtigten optionalen Wert, Fallback oder Fehler entspricht.
  • Expliziter Nullwert: Prüfe ihn unabhängig vom fehlenden Feld.
  • Bereitgestellter Wert: Verwende einen Wert ungleich dem Standard, um nachzuweisen, dass die Dekodierung ihn nicht überschreibt.
  • Inkompatibler Typ: Bestätige, dass weiterhin ein Fehler ausgelöst wird, wenn keine Konvertierungsrichtlinie existiert.
  • Älterer Leser: Prüfe, ob die überarbeitete codierte Darstellung weiterhin seinen Anforderungen entspricht.

Entferne ein Feld nicht aus CodingKeys, nur um einen Fallback zu erhalten. Apple erklärt, dass ausgelassene Eigenschaften aus der codierten und dekodierten Darstellung ausgeschlossen werden und für automatische Dekodierungskonformität Standardwerte benötigen. Das ist geeignet, wenn eine Eigenschaft absichtlich außerhalb der Darstellung bleibt; es bedeutet auch, dass ein bereitgestellter Wert für diese Eigenschaft nicht gelesen wird. Siehe Apples Hinweise zu Coding Keys.

Lege die Kompatibilitätsregel neben der Dekodierungslogik des Feldes fest: optional bei sinnvoller Abwesenheit, ein expliziter Fallback bei akzeptiertem Fehlen und gewöhnliche fehlerauslösende Dekodierung für erforderliche Werte. Die Testdaten im Speicher machen das beabsichtigte Verhalten dann bei jeder Modelländerung überprüfbar.

Quellen