Maak waarden die echt kunnen ontbreken optioneel en implementeer decodeerterugvalwaarden expliciet om een Swift Codable-model compatibel te houden met oudere representaties in het geheugen. Alleen een standaardwaarde in de declaratie toevoegen zorgt er niet voor dat een ontbrekend gecodeerd veld succesvol wordt gedecodeerd.

Deze handleiding is bedoeld voor een Mac-ontwikkelaar die een eenvoudig desktoplay-outmodel bijwerkt. De concrete taak is om het herziene model oudere synthetische representaties te laten accepteren, terwijl aangeleverde waarden behouden blijven en incompatibele typen worden gemeld. Elk voorbeeld maakt en gebruikt waarden in het geheugen.

Wat verandert er als een veld optioneel wordt of een standaardwaarde krijgt?

Codable combineert coderen en decoderen. Swift kan deze implementaties synthetiseren wanneer de opgeslagen eigenschappen de benodigde protocollen ondersteunen. Dat scheelt routinematige code, maar een wijziging die nog steeds compileert kan veranderen welke representaties het model accepteert. Apple beschrijft zowel automatische conformance als aangepaste implementaties in Aangepaste typen coderen en decoderen.

Neem een lay-outtegel met een verplichte titel en ondertitel. Je wilt de ondertitel nu optioneel maken en een nieuw aantal kolommen standaard op drie zetten. Dit zijn twee afzonderlijke keuzes: de ondertitel kan geen waarde hebben, terwijl het aantal kolommen na het decoderen altijd een bruikbare waarde moet hebben.

Voor gewone veranderlijke eigenschappen die in gesynthetiseerd decoderen zijn opgenomen, staat Swift toe dat een optionele eigenschap een ontbrekende sleutel of een null-waarde heeft. Een initialisatie in de declaratie is geen decodeerterugvalwaarde: een optionele eigenschap die met een niet-nil waarde is geïnitialiseerd kan nog steeds worden gedecodeerd als nil, en een niet-optionele eigenschap met een standaardwaarde in de declaratie kan nog steeds mislukken als de sleutel ontbreekt. De uitleg van het Swift-team over Codable-synthese beschrijft dit bewuste gedrag.

De volgende beslissingstabel vertaalt die regels naar een veldbeleid. De tabel gaat uit van een geheel-getalveld en een aanwezige waarde die naar Int kan worden omgezet. Voor de laatste twee rijen is aangepast decoderen nodig.

Veldbeleid Sleutel ontbreekt Expliciete null Geldig geheel getal Incompatibel type
Gesynthetiseerd var columns: Int Fout Fout Waarde gebruiken Fout
Gesynthetiseerd var columns: Int = 3 Fout Fout Waarde gebruiken Fout
Gesynthetiseerd var columns: Int? nil nil Waarde gebruiken Fout
Aangepaste terugvalwaarde voor ontbrekende sleutel en null 3 3 Waarde gebruiken Fout
Aangepaste terugvalwaarde alleen voor ontbrekende sleutel 3 Fout Waarde gebruiken Fout

Het verschil komt door de containermethoden. decode(_:forKey:) vereist een aanwezige sleutel en een niet-null waarde die kan worden omgezet. decodeIfPresent(_:forKey:) retourneert nil bij zowel een ontbrekende sleutel als null, maar gooit nog steeds een fout bij een incompatibel type. ?? 3 toepassen op dat laatste resultaat levert de terugvalwaarde.

Implementeer en controleer het compatibiliteitsbeleid in het geheugen

Gebruik dit voorbeeld in een Swift-uitvoeringsomgeving op je Mac waarop Foundation beschikbaar is en die instructies op topniveau ondersteunt, zoals een bestaande playground. Voer het volledige blok in één keer uit. Foundations JSONEncoder maakt een Data-representatie, en JSONDecoder maakt daaruit een nieuwe getypeerde waarde. Het voorbeeld gebruikt geen letterlijke gecodeerde documentinhoud of externe invoer.

Het gekozen beleid is expliciet: title blijft verplicht; een ontbrekende of null subtitle wordt nil; een ontbrekende of null columns wordt drie. Een aanwezige incompatibele kolomwaarde blijft een fout. De aangepaste initializer verzorgt het decoderen, terwijl coderen onder de CodingKeys van het model gesynthetiseerd blijft. Apple beschrijft deze aanpassingsmethode in Aangepaste typen coderen en decoderen.

Dit is een volledig programma, inclusief synthetische fixtures en controles. Swift was niet beschikbaar in de onderzoeksomgeving, dus het voorbeeld is gecontroleerd aan de hand van de gedocumenteerde API's, maar niet uitgevoerd.

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.")

De verwachte uiteindelijke uitvoer is All in-memory compatibility checks passed. Onverwachte fouten worden doorgegeven in plaats van als succes te worden behandeld. De fixtures bestrijken bewust verschillende representaties: MinimalTile laat velden weg, terwijl NullTile encodeNil(forKey:) gebruikt om expliciete null-vermeldingen te maken.

init(from:) in een extensie houden zorgt er ook voor dat de memberwise-initializer van de structuur beschikbaar blijft, zodat het voorbeeld Tile(title:subtitle:columns:) kan maken. De aangepaste decodeerinitializer wijst elke opgeslagen instantie-eigenschap toe, zodat het resultaat volledig is geïnitialiseerd voordat de initializer terugkeert.

Moet null de standaardwaarde gebruiken of een fout blijven?

Kies op basis van de betekenis van het veld. Voor deze illustratieve lay-out is het redelijk om zowel een ontbrekende waarde als null te behandelen als het standaard aantal kolommen. Het blijft een keuze voor de applicatie: decodeIfPresent combineert die twee toestanden bewust, waarna de terugvalwaarde voor beide hetzelfde resultaat oplevert. Het gedocumenteerde retourgedrag maakt dat gevolg expliciet.

Als afwezigheid betekent “gebruik de standaardlay-out”, maar een aangeleverde null moet mislukken, vervang dan alleen de toewijzing columns binnen de init(from:) van het programma door dit fragment. Alle overige vereisten en definities blijven hetzelfde.

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

contains(_:) geeft aan of er een vermelding bestaat, ook als die null is. Daarom volgt een expliciete null de tak decode en ontstaat valueNotFound; alleen afwezigheid krijgt de standaardwaarde. Volgens dit herziene beleid moet de controle voor de null-fixture in het oorspronkelijke programma worden aangepast om die fout te verwachten in plaats van gelijkheid met minimal.

Maak de toewijzing niet (try? values.decode(Int.self, forKey: .columns)) ?? 3, tenzij het de bedoeling is om elke decodeerfout te negeren. Die expressie vervangt ook een aangeleverde incompatibele waarde door drie. try decodeIfPresent gebruiken behoudt het onderscheid tussen een geaccepteerde weglating en een representatie die het veld niet kan interpreteren, zoals vastgelegd in het foutcontract van de decodeermethode.

Werkt compatibiliteit in beide richtingen?

Controleer elke lezer afzonderlijk. Het programma laat zien dat de herziene lezer de oudere representatie accepteert. Het laat ook een grens zien: de oudere lezer vereist nog steeds subtitle en wijst daarom een nieuwere representatie af waarin dat veld ontbreekt. De nieuwe Swift-eigenschap optioneel maken verandert de vereisten van het oude type niet.

Gesynthetiseerd coderen van optionele waarden laat nil-waarden weg via voorwaardelijk coderen. Swifts encodeIfPresent(_:forKey:) documenteert dat alleen niet-nil waarden worden gecodeerd. Een expliciete null coderen kan met encodeNil, maar daarmee kan nog steeds niet worden voldaan aan een oudere niet-optionele String-eigenschap die een waarde vereist.

Wanneer een oudere lezer bruikbaar moet blijven, definieer dan een expliciete representatie die aan de vereisten voldoet. In dit synthetische voorbeeld kan een adapter een LegacyTile maken met een gekozen ondertitel voordat deze wordt gecodeerd. Of een ontbrekende ondertitel een bepaalde tekenreeks moet worden, is een afzonderlijke productkeuze; verzin die betekenis niet stilzwijgend in een algemene terugvalwaarde.

Maak ook onderscheid tussen het behouden van waarden en het behouden van representaties. In het voorbeeld worden een ontbrekende ondertitel en een expliciete null dezelfde Swift-waarde: nil. Het model kan dus niet reproduceren welke van die twee invoeren het heeft ontvangen. Ook levert het decoderen van een ontbrekend aantal kolommen als drie, gevolgd door het opnieuw coderen van het model, een concreet aantal kolommen op. Dit zijn rechtstreekse gevolgen van het gekozen model en decodeerbeleid.

Wat moet je controleren voordat je het model wijzigt?

Gebruik telkens wanneer een veld verandert een kleine compatibiliteitsmatrix. Begin met een oudere representatie en varieer daarna één veld tegelijk. Zo kan een geslaagde roundtrip geen fout verbergen die alleen optreedt wanneer een sleutel ontbreekt.

  • Oudere invoer: controleer of de herziene lezer deze accepteert en bestaande waarden behoudt.
  • Ontbrekend veld: controleer of het resultaat de bedoelde optionele waarde, terugvalwaarde of fout is.
  • Expliciete null: test deze apart van weglating.
  • Aangeleverde waarde: gebruik een niet-standaardwaarde om te bewijzen dat decoderen deze niet overschrijft.
  • Incompatibel type: controleer of er nog steeds een fout wordt gegooid wanneer er geen conversiebeleid is.
  • Oudere lezer: controleer of de gecodeerde representatie na de wijziging nog steeds aan de vereisten voldoet.

Verwijder een veld niet uit CodingKeys alleen om een terugvalwaarde te krijgen. Apple legt uit dat weggelaten eigenschappen buiten de gecodeerde en gedecodeerde representatie vallen en standaardwaarden nodig hebben voor automatische decodeerconformance. Dat is geschikt wanneer een eigenschap bewust buiten de representatie blijft; het betekent ook dat een aangeleverde waarde voor die eigenschap niet wordt gelezen. Zie Apples richtlijnen voor coding keys.

Houd de compatibiliteitsregel bij de decodeerlogica van het veld: optioneel voor betekenisvolle afwezigheid, een expliciete terugvalwaarde voor een geaccepteerde weglating en gewone foutgevende decodering voor verplichte waarden. De in-memoryfixtures maken het bedoelde gedrag vervolgens controleerbaar telkens wanneer het model verandert.

Bronnen