Pour qu’un modèle Swift Codable reste compatible avec les anciennes représentations en mémoire, rendez facultatives les valeurs réellement absentes et implémentez explicitement les valeurs de repli lors du décodage. Une valeur par défaut dans la déclaration ne suffit pas à décoder correctement un champ encodé manquant.

Ce guide s’adresse aux développeurs Mac qui mettent à jour un modèle simple de disposition pour ordinateur. L’objectif concret est de permettre au modèle révisé d’accepter d’anciennes représentations synthétiques, tout en préservant les valeurs fournies et en signalant les types incompatibles. Chaque exemple crée et utilise des valeurs en mémoire.

Que se passe-t-il lorsqu’un champ devient facultatif ou reçoit une valeur par défaut ?

Codable combine l’encodage et le décodage. Swift peut synthétiser ces implémentations lorsque les propriétés stockées prennent en charge les protocoles requis. Cela évite le code répétitif, mais une modification qui compile toujours peut changer les représentations acceptées par le modèle. Apple documente la conformité automatique et les implémentations personnalisées dans Encoding and Decoding Custom Types.

Prenons une tuile de disposition avec un titre et un sous-titre obligatoires. Vous souhaitez maintenant rendre le sous-titre facultatif et donner par défaut la valeur trois au nombre de colonnes. Il s’agit de deux décisions distinctes : le sous-titre peut ne pas avoir de valeur, tandis que le nombre de colonnes doit toujours avoir une valeur exploitable après le décodage.

Pour les propriétés mutables ordinaires incluses dans le décodage synthétisé, Swift considère qu’une propriété facultative autorise l’absence de clé ou une valeur null. Un initialiseur déclaré ne constitue pas une valeur de repli au décodage : une propriété facultative initialisée avec une valeur non nulle peut malgré tout être décodée comme nil, et une propriété non facultative avec une valeur par défaut dans sa déclaration peut tout de même échouer si sa clé est absente. L’explication de la synthèse de Codable de l’équipe Swift décrit ce comportement délibéré.

Le tableau de décision suivant traduit ces règles en politique pour un champ. Il suppose un champ entier et une valeur présente convertible en Int. Les deux dernières lignes nécessitent un décodage personnalisé.

Politique du champ Clé absente Null explicite Entier valide Type incompatible
var columns: Int synthétisé Erreur Erreur Utiliser la valeur Erreur
var columns: Int = 3 synthétisé Erreur Erreur Utiliser la valeur Erreur
var columns: Int? synthétisé nil nil Utiliser la valeur Erreur
Valeur de repli personnalisée en cas d’absence ou de null 3 3 Utiliser la valeur Erreur
Valeur de repli personnalisée en cas d’absence uniquement 3 Erreur Utiliser la valeur Erreur

La différence vient des méthodes du conteneur. decode(_:forKey:) exige une entrée et une valeur non nulle convertible. decodeIfPresent(_:forKey:) renvoie nil en cas d’absence ou de valeur null, mais lève toujours une erreur si le type est incompatible. Appliquer ?? 3 au résultat de cette dernière méthode fournit la valeur de repli.

Implémentez et vérifiez la politique de compatibilité en mémoire

Utilisez cet exemple dans un environnement d’exécution Swift sur votre Mac avec Foundation disponible et prenant en charge les instructions de premier niveau, comme un playground existant. Exécutez le bloc entier. JSONEncoder de Foundation produit une représentation Data, et JSONDecoder crée une nouvelle valeur typée à partir de celle-ci. L’exemple n’utilise aucun document encodé littéral ni aucune entrée externe.

La politique choisie est explicite : title reste obligatoire ; si subtitle est absent ou null, il devient nil ; si columns est absent ou null, sa valeur devient trois. Une valeur de colonne présente mais incompatible reste une erreur. L’initialiseur personnalisé gère le décodage, tandis que l’encodage reste synthétisé selon les CodingKeys du modèle. Apple décrit cette méthode de personnalisation dans Encoding and Decoding Custom Types.

Il s’agit d’un programme complet, avec des exemples synthétiques et des vérifications. Swift n’était pas disponible dans l’environnement de recherche ; l’exemple a donc été vérifié au regard des API documentées, mais n’a pas été exécuté.

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

Le résultat final attendu est All in-memory compatibility checks passed. Les erreurs inattendues sont propagées au lieu d’être converties en succès. Les exemples couvrent volontairement différentes représentations : MinimalTile omet des champs, tandis que NullTile utilise encodeNil(forKey:) pour créer des entrées explicitement nulles.

Conserver init(from:) dans une extension permet aussi de garder disponible l’initialiseur membre de la structure, ce qui permet à l’exemple de construire Tile(title:subtitle:columns:). L’initialiseur de décodage personnalisé attribue une valeur à chaque propriété d’instance stockée ; le résultat est donc entièrement initialisé avant son retour.

Une valeur null doit-elle utiliser la valeur par défaut ou rester une erreur ?

Choisissez en fonction de la signification du champ. Pour cette disposition illustrative, traiter l’absence et la valeur null comme le nombre standard de colonnes est une politique raisonnable. Cela reste une décision propre à l’application : decodeIfPresent combine volontairement ces deux états, et la valeur de repli leur donne ensuite le même résultat. Son comportement de retour documenté rend cette conséquence explicite.

Si l’absence signifie « utiliser la disposition standard », mais qu’une valeur null fournie doit entraîner un échec, remplacez uniquement l’affectation columns dans le init(from:) du programme par ce fragment. Tous les autres prérequis et définitions restent identiques.

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

contains(_:) indique si une entrée existe, y compris une entrée nulle. Par conséquent, une valeur null explicite emprunte la branche decode et produit valueNotFound ; seule l’absence reçoit la valeur par défaut. Avec cette politique révisée, la vérification de l’exemple null du programme d’origine doit être modifiée pour attendre cette erreur plutôt qu’une égalité avec minimal.

Évitez de transformer l’affectation en (try? values.decode(Int.self, forKey: .columns)) ?? 3, sauf si le comportement souhaité est de supprimer toutes les erreurs de décodage. Cette expression remplace aussi par trois une valeur fournie mais incompatible. Utiliser try decodeIfPresent préserve la distinction entre une omission acceptée et une représentation que le champ ne peut pas interpréter, conformément au contrat d’erreur de la méthode de décodage.

La compatibilité fonctionne-t-elle dans les deux sens ?

Vérifiez séparément chaque lecteur. Le programme montre que le lecteur révisé accepte l’ancienne représentation. Il montre aussi une limite : l’ancien lecteur exige toujours subtitle ; il rejette donc une représentation plus récente dans laquelle ce champ est omis. Rendre facultative la nouvelle propriété Swift ne change pas les exigences de l’ancien type.

L’encodage synthétisé d’une propriété facultative omet les valeurs nil au moyen d’un encodage conditionnel. Swift documente que encodeIfPresent(_:forKey:) n’encode que les valeurs non nil. Il est aussi possible d’encoder explicitement une valeur null avec encodeNil, mais cela ne satisfait toujours pas une ancienne propriété String non facultative qui exige une valeur.

Lorsqu’un ancien lecteur doit rester utilisable, définissez une représentation explicite qui répond à ses exigences. Dans cet exemple synthétique, un adaptateur pourrait créer un LegacyTile avec un sous-titre choisi avant l’encodage. Décider si un sous-titre absent doit devenir une chaîne particulière relève d’une décision produit distincte ; n’inventez pas silencieusement cette signification dans une valeur de repli générale.

Distinguez également la préservation des valeurs de celle des représentations. Dans l’exemple, un sous-titre omis et une valeur null explicite deviennent la même valeur Swift, nil. Le modèle ne peut donc pas reproduire laquelle de ces deux entrées il a reçue. De même, décoder un nombre de colonnes manquant en lui attribuant la valeur trois, puis réencoder le modèle, fournit un nombre de colonnes concret. Ces conséquences découlent directement du modèle et de la politique de décodage choisis.

Que faut-il vérifier avant de modifier le modèle ?

Utilisez une petite matrice de compatibilité chaque fois qu’un champ change. Commencez par une ancienne représentation, puis faites varier un champ à la fois. Ainsi, une conversion aller-retour réussie ne masquera pas un échec qui ne se produit que lorsqu’une clé manque.

  • Ancienne entrée : vérifiez que le lecteur révisé l’accepte et préserve les valeurs existantes.
  • Champ manquant : vérifiez que le résultat est la valeur facultative, la valeur de repli ou l’erreur prévue.
  • Valeur null explicite : testez-la séparément de l’omission.
  • Valeur fournie : utilisez une valeur différente de la valeur par défaut pour vérifier que le décodage ne la remplace pas.
  • Type incompatible : vérifiez que l’opération lève toujours une erreur lorsqu’aucune règle de conversion n’existe.
  • Ancien lecteur : vérifiez si la représentation encodée révisée répond toujours à ses exigences.

Ne retirez pas un champ de CodingKeys uniquement pour obtenir une valeur de repli. Apple explique que les propriétés omises sont exclues de la représentation encodée et décodée, et qu’elles doivent avoir des valeurs par défaut pour bénéficier de la conformité automatique au décodage. Cela convient lorsqu’une propriété doit intentionnellement rester hors de la représentation ; cela signifie aussi qu’une valeur fournie pour cette propriété ne sera pas lue. Consultez les indications d’Apple sur les clés de codage.

Placez la règle de compatibilité près de la logique de décodage du champ : facultatif en cas d’absence significative, valeur de repli explicite pour une omission acceptée et décodage classique avec levée d’erreur pour les valeurs obligatoires. Les exemples en mémoire rendent alors le comportement attendu vérifiable chaque fois que le modèle change.

Sources