Per mantenere un modello Swift Codable compatibile con le rappresentazioni in memoria precedenti, rendi opzionali i valori effettivamente assenti e implementa esplicitamente i ripieghi di decodifica. La sola aggiunta di un valore predefinito alla dichiarazione non consente di decodificare correttamente un campo codificato mancante.

Questa guida è rivolta a chi sviluppa app per Mac e sta aggiornando un semplice modello di layout desktop. L'obiettivo concreto è consentire al modello aggiornato di accettare rappresentazioni sintetiche precedenti, preservando i valori forniti e segnalando i tipi incompatibili. Ogni esempio crea e usa valori in memoria.

Cosa cambia quando un campo diventa opzionale o riceve un valore predefinito?

Codable combina codifica e decodifica. Swift può sintetizzare queste implementazioni quando le proprietà memorizzate supportano i protocolli necessari. Ciò elimina il codice di routine, ma una modifica che continua a compilare può alterare le rappresentazioni accettate dal modello. Apple documenta sia la conformità automatica sia le implementazioni personalizzate in Codifica e decodifica di tipi personalizzati.

Considera una tessera di layout con un titolo e un sottotitolo obbligatori. Ora vuoi che il sottotitolo sia opzionale e che il nuovo numero di colonne abbia un valore predefinito pari a tre. Sono due decisioni distinte: il sottotitolo può essere privo di valore, mentre il numero di colonne deve sempre avere un valore utilizzabile dopo la decodifica.

Per le normali proprietà modificabili incluse nella decodifica sintetizzata, Swift considera una proprietà opzionale come compatibile con una chiave mancante o con un valore nullo. Un inizializzatore nella dichiarazione non è un ripiego di decodifica: una proprietà opzionale inizializzata con un valore non nullo può comunque essere decodificata come nil, e una proprietà non opzionale con un valore predefinito nella dichiarazione può comunque fallire se la chiave è assente. La spiegazione della sintesi di Codable del team Swift descrive questo comportamento intenzionale.

La tabella decisionale seguente traduce queste regole in una politica per i campi. Presuppone un campo intero e un valore presente convertibile in Int. Le ultime due righe richiedono una decodifica personalizzata.

Politica del campo Chiave assente Null esplicito Intero valido Tipo incompatibile
var columns: Int sintetizzato Errore Errore Usa il valore Errore
var columns: Int = 3 sintetizzato Errore Errore Usa il valore Errore
var columns: Int? sintetizzato nil nil Usa il valore Errore
Ripiego personalizzato per assenza e null 3 3 Usa il valore Errore
Ripiego personalizzato solo per assenza 3 Errore Usa il valore Errore

La distinzione dipende dai metodi del contenitore. decode(_:forKey:) richiede una voce e un valore non nullo convertibile. decodeIfPresent(_:forKey:) restituisce nil sia in caso di assenza sia in caso di null, ma genera comunque un errore se il tipo è incompatibile. Applicando ?? 3 al risultato si fornisce il valore di ripiego.

Implementare e verificare in memoria la politica di compatibilità

Usa questo esempio in un contesto di esecuzione Swift sul tuo Mac con Foundation disponibile e supporto per le istruzioni di livello superiore, ad esempio un playground esistente. Esegui l'intero blocco insieme. JSONEncoder di Foundation produce una rappresentazione Data, mentre JSONDecoder crea un nuovo valore tipizzato a partire da essa. L'esempio non usa documenti codificati letterali né input esterni.

La politica scelta è esplicita: title resta obbligatorio; se subtitle è assente o null diventa nil; se columns è assente o null diventa tre. Se è presente un valore incompatibile per le colonne, si verifica comunque un errore. L'inizializzatore personalizzato gestisce la decodifica, mentre la codifica resta sintetizzata in base alle CodingKeys del modello. Apple documenta questo approccio di personalizzazione in Codifica e decodifica di tipi personalizzati.

Questo è un programma completo, con dati di prova sintetici e verifiche. Swift non era disponibile nell'ambiente di ricerca, quindi l'esempio è stato confrontato con le API documentate, ma non è stato eseguito.

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

L'output finale previsto è All in-memory compatibility checks passed. Gli errori imprevisti vengono propagati invece di essere convertiti in esito positivo. I dati di prova coprono intenzionalmente rappresentazioni diverse: MinimalTile omette i campi, mentre NullTile usa encodeNil(forKey:) per creare voci esplicite con valore null.

Mantenere init(from:) in un'estensione lascia inoltre disponibile l'inizializzatore membro per la struttura, consentendo all'esempio di costruire Tile(title:subtitle:columns:). L'inizializzatore di decodifica personalizzato assegna ogni proprietà di istanza memorizzata, quindi il risultato è completamente inizializzato prima di essere restituito.

Il valore null dovrebbe usare il valore predefinito o restare un errore?

Scegli in base al significato del campo. Per questo layout illustrativo, considerare sia l'assenza sia il valore null come il numero standard di colonne è una politica ragionevole. Resta comunque una decisione applicativa: decodeIfPresent combina intenzionalmente questi due stati, e il ripiego assegna loro lo stesso risultato. Il comportamento di restituzione documentato chiarisce questa conseguenza.

Se l'assenza significa «usa il layout standard», ma un null fornito deve causare un errore, sostituisci solo l'assegnazione columns all'interno di init(from:) del programma con questo frammento. Tutti gli altri prerequisiti e definizioni restano invariati.

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

contains(_:) indica se una voce esiste, anche quando il suo valore è null. Di conseguenza, il null esplicito segue il ramo decode e genera valueNotFound; solo l'assenza riceve il valore predefinito. Con questa politica aggiornata, la verifica nel programma originale per i dati di prova null deve aspettarsi quell'errore, anziché l'uguaglianza con minimal.

Evita di trasformare l'assegnazione in (try? values.decode(Int.self, forKey: .columns)) ?? 3, a meno che il comportamento desiderato non sia ignorare ogni errore di decodifica. Questa espressione sostituisce tre anche a un valore fornito di tipo incompatibile. L'uso di try decodeIfPresent mantiene la distinzione tra un'omissione accettata e una rappresentazione che il campo non può interpretare, come specificato dal contratto sugli errori del metodo di decodifica.

La compatibilità funziona in entrambe le direzioni?

Verifica separatamente ciascun lettore. Il programma dimostra che il lettore aggiornato accetta la rappresentazione precedente. Mostra anche un limite: il lettore precedente continua a richiedere subtitle, quindi rifiuta una rappresentazione più recente in cui quel campo è omesso. Rendere opzionale la nuova proprietà Swift non modifica i requisiti del vecchio tipo.

La codifica sintetizzata dei valori opzionali omette quelli nil tramite codifica condizionale. Swift documenta encodeIfPresent(_:forKey:), che codifica solo i valori non nil. È possibile codificare esplicitamente un valore null con encodeNil, ma ciò non soddisfa comunque una vecchia proprietà non opzionale String che richiede un valore.

Quando è necessario mantenere utilizzabile un lettore precedente, definisci una rappresentazione esplicita che ne soddisfi i requisiti. In questo esempio sintetico, un adattatore potrebbe costruire un LegacyTile con un sottotitolo scelto prima della codifica. Stabilire se un sottotitolo assente debba diventare una stringa specifica è una decisione di prodotto distinta: non inventare tacitamente quel significato all'interno di un ripiego generico.

Distingui anche la conservazione del valore dalla conservazione della rappresentazione. Nell'esempio, un sottotitolo omesso e un null esplicito diventano entrambi lo stesso valore Swift, nil. Il modello quindi non può riprodurre quale dei due input ha ricevuto. Analogamente, decodificare come tre un numero di colonne mancante e ricodificare il modello inserisce un numero di colonne concreto. Queste sono conseguenze dirette del modello e della politica di decodifica scelti.

Cosa verificare prima di modificare il modello?

Usa una piccola matrice di compatibilità ogni volta che cambia un campo. Parti da una rappresentazione precedente, poi varia un campo alla volta. In questo modo, un round trip riuscito non nasconde un errore che si verifica solo quando manca una chiave.

  • Input precedente: verifica che il lettore aggiornato lo accetti e preservi i valori esistenti.
  • Campo mancante: verifica che il risultato sia il valore opzionale, il ripiego o l'errore previsto.
  • Null esplicito: testalo separatamente dall'omissione.
  • Valore fornito: usa un valore diverso da quello predefinito per dimostrare che la decodifica non lo sovrascrive.
  • Tipo incompatibile: verifica che venga comunque generato un errore quando non esiste una politica di conversione.
  • Lettore precedente: verifica se la rappresentazione codificata aggiornata continua a soddisfarne i requisiti.

Non rimuovere un campo da CodingKeys solo per ottenere un ripiego. Apple spiega che le proprietà omesse vengono escluse dalla rappresentazione codificata e decodificata e che, per la decodifica automatica, devono avere valori predefiniti. È una scelta adatta quando una proprietà deve restare intenzionalmente fuori dalla rappresentazione; significa anche che il valore fornito per quella proprietà non verrà letto. Consulta le indicazioni di Apple sulle chiavi di codifica.

Mantieni la regola di compatibilità accanto alla logica di decodifica del campo: opzionale quando l'assenza ha significato, ripiego esplicito per un'omissione accettata e decodifica ordinaria che genera errori per i valori obbligatori. I dati di prova in memoria rendono così verificabile il comportamento previsto ogni volta che il modello cambia.

Fonti