Agar model Codable Swift tetap kompatibel dengan representasi dalam memori yang lebih lama, jadikan nilai yang memang tidak ada sebagai opsional dan implementasikan fallback decoding secara eksplisit. Menambahkan nilai default pada deklarasi saja tidak membuat decoding berhasil jika field yang dikodekan tidak ada.

Panduan ini ditujukan bagi pengembang Mac yang memperbarui model tata letak desktop sederhana. Tugas konkretnya adalah membuat model yang telah direvisi menerima representasi sintetis lama sekaligus mempertahankan nilai yang diberikan dan melaporkan tipe yang tidak kompatibel. Setiap contoh membuat dan menggunakan nilai di memori.

Apa yang berubah saat sebuah field menjadi opsional atau mendapat nilai default?

Codable menggabungkan encoding dan decoding. Swift dapat menyintesis implementasi tersebut saat properti tersimpan mendukung protokol yang diperlukan. Ini menghilangkan kode rutin, tetapi perubahan yang tetap berhasil dikompilasi dapat mengubah representasi mana yang diterima model. Apple menjelaskan konformansi otomatis dan implementasi khusus dalam Encoding and Decoding Custom Types.

Bayangkan sebuah ubin tata letak dengan judul dan subjudul wajib. Kini Anda ingin subjudulnya opsional dan jumlah kolom baru memiliki nilai default tiga. Ini adalah dua keputusan terpisah: subjudul boleh tidak memiliki nilai, sedangkan jumlah kolom harus selalu memiliki nilai yang dapat digunakan setelah decoding.

Untuk properti mutable biasa yang disertakan dalam decoding hasil sintesis, Swift memperlakukan properti opsional sebagai properti yang mengizinkan kunci tidak ada atau nilai null. Inisialisasi pada deklarasi bukan fallback decoding: properti opsional yang diinisialisasi dengan nilai bukan-nil masih dapat di-decode sebagai nil, dan properti nonopsional dengan nilai default pada deklarasi masih dapat gagal jika kuncinya tidak ada. Penjelasan tim Swift tentang sintesis Codable menguraikan perilaku yang disengaja ini.

Tabel keputusan berikut menerjemahkan aturan tersebut menjadi kebijakan field. Tabel ini mengasumsikan field integer dan nilai yang ada dapat dikonversi menjadi Int. Dua baris terakhir memerlukan decoding khusus.

Kebijakan field Kunci tidak ada Null eksplisit Integer valid Tipe tidak kompatibel
Sintesis var columns: Int Error Error Gunakan nilai Error
Sintesis var columns: Int = 3 Error Error Gunakan nilai Error
Sintesis var columns: Int? nil nil Gunakan nilai Error
Fallback khusus untuk kunci tidak ada dan null 3 3 Gunakan nilai Error
Fallback khusus hanya untuk kunci tidak ada 3 Error Gunakan nilai Error

Perbedaannya berasal dari metode container. decode(_:forKey:) mewajibkan adanya entri dan nilai non-null yang dapat dikonversi. decodeIfPresent(_:forKey:) mengembalikan nil jika kunci tidak ada maupun jika nilainya null, tetapi tetap melempar error untuk tipe yang tidak kompatibel. Menerapkan ?? 3 pada hasil tersebut menyediakan nilai fallback.

Terapkan dan periksa kebijakan kompatibilitas di memori

Gunakan contoh ini dalam konteks eksekusi Swift di Mac Anda dengan Foundation tersedia dan dukungan untuk pernyataan tingkat atas, seperti playground yang sudah ada. Jalankan seluruh blok sekaligus. JSONEncoder dari Foundation menghasilkan representasi Data, dan JSONDecoder membuat nilai bertipe baru darinya. Contoh ini tidak menggunakan dokumen encoded literal atau input eksternal.

Kebijakan yang dipilih dinyatakan secara eksplisit: title tetap wajib; subtitle yang tidak ada atau bernilai null menjadi nil; columns yang tidak ada atau bernilai null menjadi tiga. Nilai kolom yang ada tetapi tidak kompatibel tetap menghasilkan error. Initializer khusus menangani decoding, sedangkan encoding tetap disintesis berdasarkan CodingKeys model. Apple menjelaskan pendekatan kustomisasi ini dalam Encoding and Decoding Custom Types.

Ini adalah program lengkap, termasuk fixture sintetis dan pemeriksaan. Swift tidak tersedia di lingkungan riset, jadi contoh ini ditinjau berdasarkan API yang didokumentasikan, tetapi tidak dijalankan.

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

Output akhir yang diharapkan adalah All in-memory compatibility checks passed. Error yang tidak terduga akan diteruskan alih-alih diubah menjadi keberhasilan. Fixture sengaja mencakup representasi yang berbeda: MinimalTile tidak menyertakan field, sedangkan NullTile menggunakan encodeNil(forKey:) untuk membuat entri null eksplisit.

Menempatkan init(from:) dalam ekstensi juga membuat initializer memberwise milik struktur tetap tersedia, sehingga contoh dapat membuat Tile(title:subtitle:columns:). Initializer decoding khusus menetapkan setiap properti instance tersimpan, sehingga hasilnya terinisialisasi sepenuhnya sebelum dikembalikan.

Apakah null sebaiknya menggunakan nilai default atau tetap menjadi error?

Pilih berdasarkan makna field. Untuk tata letak ilustratif ini, memperlakukan kunci yang tidak ada dan null sebagai jumlah kolom standar merupakan kebijakan yang masuk akal. Namun, ini tetap keputusan aplikasi: decodeIfPresent sengaja menggabungkan kedua keadaan tersebut, lalu fallback memberikan hasil yang sama untuk keduanya. Perilaku pengembalian yang didokumentasikan memperjelas konsekuensi ini.

Jika kunci yang tidak ada berarti “gunakan tata letak standar”, tetapi null yang diberikan harus gagal, ganti hanya penetapan columns di dalam init(from:) program dengan potongan berikut. Semua prasyarat dan definisi lainnya tetap sama.

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

contains(_:) melaporkan apakah entri ada, termasuk entri null. Karena itu, null eksplisit mengambil cabang decode dan menghasilkan valueNotFound; hanya kunci yang tidak ada yang mendapat nilai default. Dengan kebijakan yang telah direvisi ini, pemeriksaan fixture null pada program awal harus diubah agar mengharapkan error tersebut, bukan kesamaan dengan minimal.

Jangan ubah penetapan menjadi (try? values.decode(Int.self, forKey: .columns)) ?? 3 kecuali memang dimaksudkan untuk membuang setiap kegagalan decoding. Ekspresi itu juga mengganti nilai yang diberikan tetapi tidak kompatibel dengan angka tiga. Penggunaan try decodeIfPresent mempertahankan perbedaan antara kunci yang tidak ada dan dapat diterima dengan representasi yang tidak dapat ditafsirkan oleh field, sesuai kontrak error metode decoding.

Apakah kompatibilitas berfungsi dua arah?

Periksa setiap pembaca secara terpisah. Program ini menunjukkan bahwa pembaca yang telah direvisi menerima representasi lama. Program ini juga menunjukkan batasannya: pembaca lama tetap mewajibkan subtitle, sehingga menolak representasi baru yang menghilangkan field tersebut. Mengubah properti Swift baru menjadi opsional tidak mengubah persyaratan tipe lama.

Encoding opsional hasil sintesis menghilangkan nilai nil melalui encoding kondisional. Swift mendokumentasikan encodeIfPresent(_:forKey:) sebagai metode yang hanya mengenkode nilai non-nil. Mengenkode null secara eksplisit juga dimungkinkan dengan encodeNil, tetapi cara itu tetap tidak dapat memenuhi properti String nonopsional lama yang mewajibkan nilai.

Jika pembaca lama harus tetap dapat digunakan, buat representasi eksplisit yang memenuhi persyaratannya. Dalam contoh sintetis ini, adaptor dapat membuat LegacyTile dengan subjudul pilihan sebelum melakukan encoding. Apakah subjudul yang tidak ada sebaiknya menjadi string tertentu merupakan keputusan produk yang terpisah; jangan mengarang makna itu secara diam-diam di dalam fallback umum.

Bedakan juga antara mempertahankan nilai dan mempertahankan representasi. Dalam contoh ini, subjudul yang tidak ada dan null eksplisit sama-sama menjadi nilai Swift yang sama, yaitu nil. Karena itu, model tidak dapat mereproduksi masukan mana dari kedua jenis tersebut yang diterimanya. Demikian pula, mendekode jumlah kolom yang tidak ada sebagai tiga lalu melakukan encoding ulang pada model akan menyertakan jumlah kolom konkret. Ini merupakan konsekuensi langsung dari model dan kebijakan decoding yang dipilih.

Apa yang perlu diverifikasi sebelum mengubah model?

Gunakan matriks kompatibilitas kecil setiap kali sebuah field berubah. Mulailah dengan representasi lama, lalu ubah satu field setiap kali. Dengan begitu, round trip yang berhasil tidak menyamarkan kegagalan yang hanya terjadi saat sebuah kunci tidak ada.

  • Input lama: Pastikan pembaca yang telah direvisi menerimanya dan mempertahankan nilai yang ada.
  • Field tidak ada: Pastikan hasilnya berupa nilai opsional, fallback, atau error yang diinginkan.
  • Null eksplisit: Uji secara terpisah dari kunci yang tidak ada.
  • Nilai yang diberikan: Gunakan nilai nondefault untuk membuktikan bahwa decoding tidak menimpanya.
  • Tipe tidak kompatibel: Pastikan metode tetap melempar error jika tidak ada kebijakan konversi.
  • Pembaca lama: Periksa apakah representasi hasil encoding yang telah direvisi tetap memenuhi persyaratannya.

Jangan menghapus field dari CodingKeys hanya untuk memperoleh fallback. Apple menjelaskan bahwa properti yang dihilangkan tidak disertakan dalam representasi hasil encoding maupun decoding, dan memerlukan nilai default agar konformansi decoding otomatis berlaku. Cara ini cocok jika sebuah properti memang sengaja berada di luar representasi; konsekuensinya, nilai yang diberikan untuk properti tersebut tidak akan dibaca. Lihat panduan coding key dari Apple.

Letakkan aturan kompatibilitas di dekat logika decoding field: opsional untuk ketiadaan yang bermakna, fallback eksplisit untuk kunci yang tidak ada dan dapat diterima, serta decoding biasa yang melempar error untuk nilai wajib. Fixture dalam memori kemudian membuat perilaku yang dimaksud mudah ditinjau setiap kali model berubah.

Sumber