Aby zachować zgodność modelu Swift Codable ze starszymi reprezentacjami w pamięci, ustaw wartości, których rzeczywiście może brakować, jako opcjonalne i jawnie zaimplementuj wartości zastępcze podczas dekodowania. Samo dodanie wartości domyślnej w deklaracji nie sprawi, że dekodowanie powiedzie się, gdy brakuje zakodowanego pola.
Ten poradnik jest przeznaczony dla programisty Mac aktualizującego prosty model układu pulpitu. Celem jest umożliwienie zaktualizowanemu modelowi przyjmowania starszych, syntetycznych reprezentacji, przy jednoczesnym zachowaniu dostarczonych wartości i zgłaszaniu niezgodnych typów. Każdy przykład tworzy i wykorzystuje wartości w pamięci.
Co się zmienia, gdy pole staje się opcjonalne lub otrzymuje wartość domyślną?
Codable łączy kodowanie i dekodowanie. Swift może automatycznie wygenerować te implementacje, gdy przechowywane właściwości obsługują wymagane protokoły. Eliminuje to rutynowy kod, ale zmiana, która nadal się kompiluje, może zmienić zakres reprezentacji akceptowanych przez model. Apple opisuje zarówno automatyczne dopasowanie do protokołu, jak i implementacje niestandardowe w artykule Kodowanie i dekodowanie typów niestandardowych.
Rozważmy kafelek układu z wymaganym tytułem i podtytułem. Teraz chcesz, aby podtytuł był opcjonalny, a nowa liczba kolumn domyślnie wynosiła trzy. To dwie odrębne decyzje: podtytuł może nie mieć wartości, natomiast liczba kolumn po dekodowaniu musi zawsze mieć użyteczną wartość.
W przypadku zwykłych zmiennych właściwości uwzględnionych w automatycznie generowanym dekodowaniu Swift traktuje właściwość opcjonalną jako dopuszczającą brak klucza lub wartość null. Inicjalizator w deklaracji nie jest wartością zastępczą używaną podczas dekodowania: właściwość opcjonalna zainicjalizowana wartością inną niż nil nadal może zostać zdekodowana jako nil, a właściwość nieopcjonalna z domyślną wartością w deklaracji nadal może spowodować błąd, gdy brakuje jej klucza. [Wyjaśnienie automatycznego generowania Codable](https://forums.swift.org/t/decoding-of-optionals-missing-in-json/52475/5 zespołu Swift opisuje to zamierzone zachowanie.
Poniższa tabela decyzyjna przekłada te zasady na reguły dotyczące pól. Zakłada pole typu całkowitoliczbowego oraz obecną wartość, którą można przekonwertować na Int. Dwa ostatnie wiersze wymagają niestandardowego dekodowania.
| Reguła dotycząca pola | Brak klucza | Jawny null | Prawidłowa liczba całkowita | Niezgodny typ |
|---|---|---|---|---|
Automatycznie generowane var columns: Int |
Błąd | Błąd | Użyj wartości | Błąd |
Automatycznie generowane var columns: Int = 3 |
Błąd | Błąd | Użyj wartości | Błąd |
Automatycznie generowane var columns: Int? |
nil |
nil |
Użyj wartości | Błąd |
| Niestandardowa wartość zastępcza przy braku i null | 3 |
3 |
Użyj wartości | Błąd |
| Niestandardowa wartość zastępcza tylko przy braku | 3 |
Błąd | Użyj wartości | Błąd |
Różnica wynika z metod kontenera. decode(_:forKey:) wymaga wpisu oraz niepustej, możliwej do przekonwertowania wartości. decodeIfPresent(_:forKey:) zwraca nil, gdy klucz nie istnieje lub ma wartość null, ale nadal zgłasza błąd przy niezgodnym typie. Zastosowanie ?? 3 do uzyskanego wyniku dostarcza wartość zastępczą.
Zaimplementuj regułę zgodności i sprawdź ją w pamięci
Użyj tego przykładu w środowisku uruchomieniowym Swift na Macu, z dostępnym Foundation i obsługą instrukcji najwyższego poziomu, na przykład w istniejącym playgroundzie. Uruchom cały blok naraz. Foundation JSONEncoder tworzy reprezentację typu Data, a JSONDecoder tworzy na jej podstawie nową wartość o określonym typie. Przykład nie korzysta z dosłownego zakodowanego dokumentu ani z zewnętrznych danych wejściowych.
Wybrana reguła jest jawna: title pozostaje wymagane; brakujące lub null subtitle staje się nil; brakujące lub null columns przyjmuje wartość trzy. Obecna wartość kolumn o niezgodnym typie nadal powoduje błąd. Niestandardowy inicjalizator obsługuje dekodowanie, natomiast kodowanie pozostaje generowane automatycznie zgodnie z CodingKeys modelu. Apple opisuje takie podejście do dostosowywania w artykule Kodowanie i dekodowanie typów niestandardowych.
To kompletny program, obejmujący syntetyczne dane testowe i sprawdzenia. W środowisku badawczym Swift był niedostępny, więc przykład sprawdzono pod kątem zgodności z udokumentowanymi interfejsami API, ale go nie uruchomiono.
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.")
Oczekiwany końcowy wynik to All in-memory compatibility checks passed. Nieoczekiwane błędy są propagowane, zamiast być zamieniane na sukces. Dane testowe celowo obejmują różne reprezentacje: MinimalTile pomija pola, a NullTile używa encodeNil(forKey:), aby utworzyć jawne wpisy null.
Pozostawienie init(from:) w rozszerzeniu zachowuje inicjalizator składowy struktury, dzięki czemu przykład może utworzyć Tile(title:subtitle:columns:). Niestandardowy inicjalizator dekodujący przypisuje wartości wszystkim przechowywanym właściwościom instancji, więc przed jego zakończeniem wynik jest w pełni zainicjalizowany.
Czy null powinien używać wartości domyślnej, czy nadal powodować błąd?
Wybierz regułę zgodnie ze znaczeniem pola. W tym przykładowym układzie rozsądne jest traktowanie zarówno braku wartości, jak i null jako standardowej liczby kolumn. To nadal decyzja na poziomie aplikacji: decodeIfPresent celowo łączy te dwa stany, a wartość zastępcza przypisuje im ten sam wynik. Jej udokumentowane zachowanie zwracania jasno pokazuje tę konsekwencję.
Jeśli brak wartości oznacza „użyj standardowego układu”, ale podany null powinien powodować błąd, zastąp w programie tylko przypisanie columns wewnątrz init(from:) tym fragmentem. Wszystkie pozostałe wymagania wstępne i definicje pozostają takie same.
// Replacement fragment inside Tile.init(from:).
if values.contains(.columns) {
columns = try values.decode(Int.self, forKey: .columns)
} else {
columns = Self.defaultColumns
}
contains(_:) informuje, czy istnieje wpis, w tym wpis o wartości null. W rezultacie jawny null trafia do gałęzi decode i powoduje valueNotFound; wartość domyślną otrzymuje tylko brakujący wpis. Zgodnie z tą zmienioną regułą sprawdzenie danych null w pierwotnym programie powinno oczekiwać tego błędu, a nie równości z minimal.
Nie zmieniaj przypisania na (try? values.decode(Int.self, forKey: .columns)) ?? 3, chyba że zamierzonym zachowaniem jest odrzucanie wszystkich błędów dekodowania. To wyrażenie podstawia też wartość trzy, gdy podana wartość ma niezgodny typ. Użycie try decodeIfPresent pozwala zachować różnicę między akceptowanym brakiem wartości a reprezentacją, której pole nie potrafi zinterpretować, zgodnie z kontraktem błędów metody dekodowania.
Czy zgodność działa w obu kierunkach?
Sprawdź osobno każdy typ odczytujący. Program pokazuje, że zaktualizowany mechanizm odczytu akceptuje starszą reprezentację. Pokazuje też ograniczenie: starszy mechanizm nadal wymaga subtitle, więc odrzuca nowszą reprezentację, w której brakuje tego pola. Zmiana nowej właściwości Swift na opcjonalną nie zmienia wymagań starszego typu.
Kodowanie opcjonalnych wartości generowane automatycznie pomija wartości nil dzięki kodowaniu warunkowemu. Swift dokumentuje, że encodeIfPresent(_:forKey:) koduje tylko wartości inne niż nil. Można zamiast tego zakodować jawny null za pomocą encodeNil, ale to nadal nie zaspokoi wymagania starszej właściwości String, która wymaga wartości.
Jeśli starszy mechanizm odczytu musi nadal działać, zdefiniuj jawną reprezentację spełniającą jego wymagania. W tym syntetycznym przykładzie adapter może utworzyć LegacyTile z wybranym podtytułem przed zakodowaniem go. To, czy brakujący podtytuł powinien przyjąć określony ciąg znaków, jest osobną decyzją produktową; nie wymyślaj po cichu takiego znaczenia w ogólnej wartości zastępczej.
Rozróżniaj też zachowanie wartości od zachowania reprezentacji. W przykładzie pominięty podtytuł i jawny null stają się tą samą wartością Swift: nil. Model nie może więc odtworzyć, który z tych dwóch rodzajów danych wejściowych otrzymał. Podobnie zdekodowanie brakującej liczby kolumn jako trzy, a następnie ponowne zakodowanie modelu powoduje zapisanie konkretnej liczby kolumn. To bezpośrednie konsekwencje wybranego modelu i reguły dekodowania.
Co sprawdzić przed zmianą modelu?
Gdy zmienia się pole, użyj niewielkiej macierzy zgodności. Zacznij od starszej reprezentacji, a następnie zmieniaj po jednym polu. Dzięki temu udana konwersja tam i z powrotem nie ukryje błędu występującego tylko wtedy, gdy brakuje klucza.
- Starsze dane wejściowe: Potwierdź, że zaktualizowany mechanizm je akceptuje i zachowuje istniejące wartości.
- Brakujące pole: Sprawdź, czy wynik to zamierzona wartość opcjonalna, wartość zastępcza lub błąd.
- Jawny null: Sprawdź go niezależnie od pominięcia pola.
- Podana wartość: Użyj wartości innej niż domyślna, aby potwierdzić, że dekodowanie jej nie nadpisuje.
- Niezgodny typ: Potwierdź, że nadal powoduje błąd, jeśli nie istnieje reguła konwersji.
- Starszy mechanizm odczytu: Sprawdź, czy zaktualizowana zakodowana reprezentacja nadal spełnia jego wymagania.
Nie usuwaj pola z CodingKeys tylko po to, by uzyskać wartość zastępczą. Apple wyjaśnia, że pominięte właściwości są wyłączane z zakodowanej i dekodowanej reprezentacji oraz wymagają wartości domyślnych, aby automatyczne dekodowanie było zgodne z protokołem. Jest to odpowiednie, gdy właściwość ma celowo pozostać poza reprezentacją; oznacza to również, że podana wartość tej właściwości nie zostanie odczytana. Zobacz wskazówki Apple dotyczące kluczy kodowania.
Umieść regułę zgodności obok logiki dekodowania danego pola: typ opcjonalny, gdy brak wartości ma znaczenie, jawna wartość zastępcza dla akceptowanego braku oraz standardowe dekodowanie zgłaszające błędy dla wartości wymaganych. Dzięki temu testowe dane w pamięci pozwolą zweryfikować zamierzone zachowanie przy każdej zmianie modelu.
Źródła
- Kodowanie i dekodowanie typów niestandardowych — Apple
- JSONEncoder — Apple
- JSONDecoder — Apple
- Dekodowanie opcjonalnych wartości brakujących w JSON — Swift Forums
- decodeIfPresent(_:forKey:) — Apple
- decode(_:forKey:) — Apple
- contains(_:) — Apple
- encodeNil(forKey:) — Apple
- encodeIfPresent(_:forKey:) — Apple