För att hålla en Swift Codable-modell kompatibel med äldre representationer i minnet gör du värden som verkligen kan saknas valfria och implementerar avkodningsreservvärden uttryckligen. Att enbart ange ett standardvärde i deklarationen gör inte att ett saknat kodat fält avkodas utan fel.
Den här guiden riktar sig till en Mac-utvecklare som uppdaterar en enkel skrivbordslayoutmodell. Den konkreta uppgiften är att låta den reviderade modellen godta äldre syntetiska representationer, samtidigt som angivna värden bevaras och inkompatibla typer rapporteras. Alla exempel skapar och använder värden i minnet.
Vad ändras när ett fält blir valfritt eller får ett standardvärde?
Codable kombinerar kodning och avkodning. Swift kan syntetisera dessa implementeringar när de lagrade egenskaperna stöder de protokoll som behövs. Det tar bort rutinmässig kod, men en ändring som fortfarande kompilerar kan påverka vilka representationer modellen godtar. Apple beskriver både automatisk konformans och anpassade implementeringar i Encoding and Decoding Custom Types.
Tänk dig en layoutbricka med en obligatorisk titel och undertitel. Nu vill du att undertiteln ska vara valfri och att ett nytt kolumnantal ska ha standardvärdet tre. Det är två separata beslut: undertiteln kan sakna värde, medan kolumnantalet alltid måste ha ett användbart värde efter avkodning.
För vanliga ändringsbara egenskaper som ingår i syntetiserad avkodning behandlar Swift en valfri egenskap som tillåtande av antingen en saknad nyckel eller ett null-värde. En initierare i deklarationen är inget reservvärde för avkodning: en valfri egenskap som initieras till ett värde som inte är nil kan ändå avkodas som nil, och en icke-valfri egenskap med ett standardvärde i deklarationen kan fortfarande ge fel om nyckeln saknas. Swift-teamets förklaring av Codable-syntes beskriver detta avsiktliga beteende.
Följande beslutstabell översätter reglerna till en fältpolicy. Den förutsätter ett heltalsfält och ett angivet värde som kan konverteras till Int. De två sista raderna kräver anpassad avkodning.
| Fältpolicy | Nyckeln saknas | Uttryckligt null | Giltigt heltal | Inkompatibel typ |
|---|---|---|---|---|
Syntetiserad var columns: Int |
Fel | Fel | Använd värdet | Fel |
Syntetiserad var columns: Int = 3 |
Fel | Fel | Använd värdet | Fel |
Syntetiserad var columns: Int? |
nil |
nil |
Använd värdet | Fel |
| Anpassat reservvärde för frånvaro och null | 3 |
3 |
Använd värdet | Fel |
| Anpassat reservvärde endast vid frånvaro | 3 |
Fel | Använd värdet | Fel |
Skillnaden beror på behållarmetoderna. decode(_:forKey:) kräver en post och ett icke-null-värde som kan konverteras. decodeIfPresent(_:forKey:) returnerar nil vid både frånvaro och null, men kastar fortfarande fel för en inkompatibel typ. Om du använder ?? 3 på det senare resultatet får du reservvärdet.
Implementera och kontrollera kompatibilitetspolicyn i minnet
Använd det här exemplet i en Swift-exekveringsmiljö på din Mac där Foundation finns tillgängligt och toppnivåsatser stöds, till exempel en befintlig playground. Kör hela kodblocket tillsammans. Foundations JSONEncoder skapar en Data-representation, och JSONDecoder skapar ett nytt typat värde från den. Exemplet använder varken ett bokstavligt kodat dokument eller extern indata.
Den valda policyn är uttrycklig: title förblir obligatorisk; en saknad eller null subtitle blir nil; en saknad eller null columns blir tre. Ett angivet kolumnvärde med inkompatibel typ ger fortfarande fel. Den anpassade initieraren hanterar avkodning, medan kodning förblir syntetiserad enligt modellens CodingKeys. Apple beskriver detta anpassningssätt i Encoding and Decoding Custom Types.
Det här är ett komplett program med syntetiska testdata och kontroller. Swift var inte tillgängligt i forskningsmiljön, så exemplet granskades mot den dokumenterade API:en men kördes inte.
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.")
Den förväntade slutliga utskriften är All in-memory compatibility checks passed. Oväntade fel skickas vidare i stället för att omvandlas till ett godkänt resultat. Testdata täcker medvetet olika representationer: MinimalTile utelämnar fält, medan NullTile använder encodeNil(forKey:) för att skapa poster med uttryckligt null-värde.
Om init(from:) ligger kvar i en extension är strukturens medlemsinitierare fortfarande tillgänglig, så att exemplet kan skapa Tile(title:subtitle:columns:). Den anpassade avkodningsinitieraren tilldelar värden till alla lagrade instansegenskaper, så resultatet är fullständigt initierat när den returnerar.
Ska null använda standardvärdet eller fortfarande ge fel?
Välj utifrån vad fältet betyder. För den här illustrativa layouten är det en rimlig policy att behandla både frånvaro och null som det vanliga kolumnantalet. Det är fortfarande ett beslut för applikationen: decodeIfPresent slår avsiktligt ihop dessa två tillstånd, och reservvärdet ger dem därför samma resultat. Den dokumenterade retursemantiken gör följden tydlig.
Om frånvaro betyder ”använd standardlayouten”, men ett angivet null-värde ska ge fel, ersätter du endast tilldelningen till columns inuti programmets init(from:) med det här kodfragmentet. Alla andra förutsättningar och definitioner är oförändrade.
// Replacement fragment inside Tile.init(from:).
if values.contains(.columns) {
columns = try values.decode(Int.self, forKey: .columns)
} else {
columns = Self.defaultColumns
}
contains(_:) anger om en post finns, även om posten har null-värde. Därför går uttryckligt null in i grenen decode och ger valueNotFound; endast frånvaro får standardvärdet. Med den reviderade policyn måste kontrollen av null-testdata i det ursprungliga programmet ändras till att förvänta sig det felet i stället för likhet med minimal.
Undvik att ändra tilldelningen till (try? values.decode(Int.self, forKey: .columns)) ?? 3 om det inte är avsikten att ignorera alla avkodningsfel. Det uttrycket ersätter även ett angivet värde med inkompatibel typ med tre. Att använda try decodeIfPresent bevarar skillnaden mellan en godtagbar utelämning och en representation som fältet inte kan tolka, i enlighet med avkodningsmetodens felkontrakt.
Fungerar kompatibiliteten i båda riktningarna?
Kontrollera varje läsare separat. Programmet visar att den reviderade läsaren godtar den äldre representationen. Det visar också en begränsning: den äldre läsaren kräver fortfarande subtitle, så den avvisar en nyare representation där fältet utelämnas. Att ändra den nya Swift-egenskapen till valfri ändrar inte kraven för den gamla typen.
Syntetiserad kodning av valfria värden utelämnar nil-värden genom villkorlig kodning. Swifts encodeIfPresent(_:forKey:) beskriver att den kodar endast värden som inte är nil. Det går att koda ett uttryckligt null-värde med encodeNil, men det tillgodoser fortfarande inte en äldre icke-valfri String-egenskap som kräver ett värde.
När en äldre läsare fortfarande måste kunna användas definierar du en uttrycklig representation som uppfyller dess krav. I det här syntetiska exemplet kan en adapter skapa en LegacyTile med en vald undertitel innan den kodas. Om en saknad undertitel ska bli en viss sträng är ett separat produktbeslut; hitta inte på den betydelsen i tysthet i ett allmänt reservvärde.
Skilj också på att bevara värden och att bevara representationer. I exemplet blir en utelämnad undertitel och ett uttryckligt null-värde samma Swift-värde, nil. Modellen kan därför inte återskapa vilket av de två indata den fick. På samma sätt ger avkodning av ett saknat kolumnantal till tre ett konkret kolumnantal när modellen kodas om. Detta är direkta följder av den valda modellen och avkodningspolicyn.
Vad bör du kontrollera innan du ändrar modellen?
Använd en liten kompatibilitetsmatris när ett fält ändras. Börja med en äldre representation och variera sedan ett fält i taget. Då kan en lyckad tur och retur inte dölja ett fel som bara uppstår när en nyckel saknas.
- Äldre indata: Bekräfta att den reviderade läsaren godtar den och bevarar befintliga värden.
- Saknat fält: Bekräfta att resultatet blir det avsedda valfria värdet, reservvärdet eller felet.
- Uttryckligt null: Testa detta separat från utelämning.
- Angivet värde: Använd ett värde som inte är standardvärdet för att visa att avkodningen inte skriver över det.
- Inkompatibel typ: Bekräfta att det fortfarande kastas ett fel när ingen konverteringspolicy finns.
- Äldre läsare: Kontrollera om den reviderade kodade representationen fortfarande uppfyller dess krav.
Ta inte bort ett fält från CodingKeys enbart för att få ett reservvärde. Apple förklarar att utelämnade egenskaper utesluts från den kodade och avkodade representationen och behöver standardvärden för automatisk avkodningskonformans. Det passar när en egenskap avsiktligt ska ligga utanför representationen; det innebär också att ett angivet värde för egenskapen inte läses. Se Apples vägledning om kodningsnycklar.
Ha kompatibilitetsregeln nära fältets avkodningslogik: valfritt när frånvaro är meningsfull, ett uttryckligt reservvärde för en godtagbar utelämning och vanlig felkastande avkodning för obligatoriska värden. Testdata i minnet gör sedan det avsedda beteendet granskningsbart varje gång modellen ändras.
Källor
- Encoding and Decoding Custom Types — Apple
- JSONEncoder — Apple
- JSONDecoder — Apple
- Decoding of optionals missing in json — Swift Forums
- decodeIfPresent(_:forKey:) — Apple
- decode(_:forKey:) — Apple
- contains(_:) — Apple
- encodeNil(forKey:) — Apple
- encodeIfPresent(_:forKey:) — Apple