Parsing dan bekerja dengan Codable di Swift 4



Format JSON telah mendapatkan popularitas besar, biasanya digunakan untuk transfer data dan eksekusi permintaan dalam aplikasi client-server. Penguraian JSON membutuhkan alat pengodean / dekode format ini, dan Apple baru-baru ini memperbaruinya. Pada artikel ini, kita akan melihat metode parsing JSON menggunakan protokol Decodable , membandingkan protokol Codable baru dengan pendahulu NSCoding , mengevaluasi kelebihan dan kekurangan, menganalisis segala sesuatu dengan contoh-contoh spesifik, dan juga mempertimbangkan beberapa fitur yang ditemukan ketika mengimplementasikan protokol.


Apa itu Codable?

Di WWDC2017, bersama dengan versi baru Swift 4, Apple memperkenalkan alat pengodean / dekode data baru yang diimplementasikan oleh tiga protokol berikut:

- Codable
- Dikodekan
- Dapat diterjemahkan

Dalam kebanyakan kasus, protokol ini digunakan untuk bekerja dengan JSON, tetapi selain itu mereka juga digunakan untuk menyimpan data ke disk, transfer melalui jaringan, dll. Encodable digunakan untuk mengonversi struktur data Swift ke objek JSON, sementara Decodable, sebaliknya, membantu mengubah objek JSON menjadi model data Swift. Protokol Codable menggabungkan dua sebelumnya dan tipikanya:

typealias Codable = Encodable & Decodable 


Untuk mematuhi protokol ini, tipe data harus menerapkan metode berikut:

Dikodekan
encode (to :) - mengkodekan model data ke dalam tipe encoder yang diberikan

Diterjemahkan
init (from :) - menginisialisasi model data dari dekoder yang disediakan

Codable
encode (ke :)
init (dari :)

Kasing sederhana

Sekarang perhatikan contoh sederhana menggunakan Codable , karena mengimplementasikan Encodable dan Decodable , dalam contoh ini Anda dapat langsung melihat semua fungsi protokol. Katakanlah kita memiliki struktur data JSON yang paling sederhana:

 { "title": "Nike shoes", "price": 10.5, "quantity": 1 } 


Model data untuk bekerja dengan JSON ini akan terlihat seperti ini:
 struct Product: Codable { var title:String var price:Double var quantity:Int enum CodingKeys: String, CodingKey { case title case price case quantity } func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(title, forKey: .title) try container.encode(price, forKey: .price) try container.encode(quantity, forKey: .quantity) } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) title = try container.decode(String.self, forKey: .title) price = try container.decode(Double.self, forKey: .price) quantity = try container.decode(Int.self, forKey: .quantity) } } 


Kedua metode yang diperlukan diimplementasikan, enumerasi juga dijelaskan untuk menentukan daftar bidang encoding / decoding. Bahkan, menulis dapat sangat disederhanakan karena Codable mendukung pembuatan kode secara otomatis (ke :) dan init (dari :), serta enumerasi yang diperlukan. Artinya, dalam hal ini, Anda dapat menulis struktur sebagai berikut:

 struct Product: Codable { var title:String var price:Double var quantity:Int } 


Sangat sederhana dan minimalis. Hanya saja, jangan lupa bahwa rekaman singkat seperti itu tidak akan berfungsi jika:

- struktur model data Anda berbeda dari yang Anda ingin encode / decode

- Anda mungkin perlu menyandikan / mendekode properti tambahan di samping properti model data Anda

- Beberapa properti model data Anda mungkin tidak mendukung protokol Codable. Dalam hal ini, Anda harus mengonversinya dari / ke protokol Codable

- seandainya nama variabel dalam model data dan nama bidang dalam wadah tidak cocok dengan Anda

Karena kami telah mempertimbangkan definisi paling sederhana dari suatu model data, ada baiknya untuk memberikan contoh kecil penggunaan praktisnya:

Jadi, dalam satu baris, Anda dapat mem-parsing respons server dalam format JSON:
 let product: Product = try! JSONDecoder().decode(Product.self, for: data) 


Dan kode berikut, sebaliknya, akan membuat objek JSON dari model data:
 let productObject = Product(title: "Cheese", price: 10.5, quantity: 1) let encodedData = try? JSONEncoder().encode(productObject) 


Semuanya sangat mudah dan cepat. Setelah mendeskripsikan model data dengan benar dan menjadikannya Codable , Anda dapat secara harfiah menyandikan / mendekode data dalam satu baris. Tapi kami menganggap model data paling sederhana yang berisi sejumlah kecil bidang tipe sederhana. Pertimbangkan masalah yang mungkin terjadi:

Tidak semua bidang dalam model data adalah Codable.

Agar model data Anda menerapkan protokol Codable, semua bidang model harus mendukung protokol ini. Secara default, protokol Codable mendukung tipe data berikut: String, Int, Double, Data, URL . Codable juga mendukung Array, Kamus, Opsional , tetapi hanya jika mengandung jenis Codable . Jika beberapa properti model data tidak sesuai dengan Codable , maka mereka harus dibawa ke sana.

 struct Pet: Codable { var name: String var age: Int var type: PetType enum CodingKeys: String, CodingKey { case name case age case type } init(from decoder: Decoder) throws { . . . } func encode(to encoder: Encoder) throws { . . . } } 


Jika dalam model data Codable kami, kami menggunakan tipe khusus, misalnya, seperti PetType , dan ingin menyandikan / mendekodekannya, maka ia juga harus mengimplementasikan init dan menyandikannya juga.

Model data tidak cocok dengan bidang JSON

Jika dalam bidang data model 3 Anda didefinisikan, misalnya, dan dalam objek JSON Anda mendapatkan 5 bidang, 2 di antaranya merupakan tambahan untuk 3 bidang tersebut, maka tidak ada yang akan berubah dalam penguraian, Anda hanya mendapatkan 3 bidang dari 5 bidang tersebut. Jika sebaliknya terjadi situasi dan dalam objek JSON akan ada setidaknya satu bidang model data, kesalahan run-time akan terjadi.
Jika beberapa bidang mungkin opsional dan secara berkala tidak ada dalam objek JSON, maka dalam hal ini perlu untuk menjadikannya opsional:

 class Product: Codable { var id: Int var productTypeId: Int? var art: String var title: String var description: String var price: Double var currencyId: Int? var brandId: Int var brand: Brand? } 


Menggunakan struktur JSON yang lebih kompleks

Seringkali respons server adalah array entitas, yaitu, Anda meminta, misalnya, daftar toko dan mendapatkan jawaban dalam bentuk:

 { "items": [ { "id": 1, "title": " ", "link": "https://www.youtube.com/watch?v=Myp6rSeCMUw", "created_at": 1497868174, "previewImage": "http://img.youtube.com/vi/Myp6rSeCMUw/mqdefault.jpg" }, { "id": 2, "title": "  2", "link": "https://www.youtube.com/watch?v=wsCEuNJmvd8", "created_at": 1525952040, "previewImage": "http://img.youtube.com/vi/wsCEuNJmvd8/mqdefault.jpg" } ] } 

Dalam hal ini, Anda dapat menulis dan mendekodekanya hanya sebagai array entitas Shop .

 struct ShopListResponse: Decodable { enum CodingKeys: String, CodingKey { case items } let items: [Shop] } 


Dalam contoh ini, fungsi otomatis init akan berfungsi, tetapi jika Anda ingin menulis dekode sendiri, Anda harus menentukan jenis yang didekodekan sebagai array:

 self.items = try container.decode([Shop].self, forKey: .items) 


Struktur Shop juga harus mengimplementasikan protokol Decodable.

 struct Shop: Decodable { var id: Int? var title: String? var address: String? var shortAddress: String? var createdAt: Date? enum CodingKeys: String, CodingKey { case id case title case address case shortAddress = "short_address" case createdAt = "created_at" } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.id = try? container.decode(Int.self, forKey: .id) self.title = try? container.decode(String.self, forKey: .title) self.address = try? container.decode(String.self, forKey: .address) self.shortAddress = try? container.decode(String.self, forKey: .shortAddress) self.createdAt = try? container.decode(Date.self, forKey: .createdAt) } } 


Parsing array elemen ini akan terlihat seperti ini:

 let parsedResult: ShopListResponse = try? JSONDecoder().decode(ShopListResponse.self, from: data) 


Dengan demikian, Anda dapat dengan mudah bekerja dengan array model data dan menggunakannya di dalam model lain.

Format tanggal

Dalam contoh ini, ada satu lagi nuansa, di sini kita pertama kali menemukan penggunaan tipe Date . Saat menggunakan tipe ini, mungkin ada masalah dengan pengkodean tanggal, dan biasanya masalah ini konsisten dengan backend. Format default adalah .deferToDate :

 struct MyDate : Encodable { let date: Date } let myDate = MyDate(date: Date()) try! encoder.encode(foo) 


myDate akan terlihat seperti ini:
 { "date" : 519751611.12542897 } 


Jika kita perlu menggunakan, misalnya, format .iso8601 , maka kita dapat dengan mudah mengubah format menggunakan properti dateEncodingStrategy :

 encoder.dateEncodingStrategy = .iso8601 


Sekarang tanggalnya akan terlihat seperti ini:

 { "date" : "2017-06-21T15:29:32Z" } 

Anda juga dapat menggunakan format tanggal khusus atau bahkan menulis dekoder tanggal Anda sendiri menggunakan opsi pemformatan berikut:

.formatted (DateFormatter) - format dekoder tanggalnya sendiri
.custom ((Date, Encoder) throws -> Void) - buat format decoding tanggal Anda sendiri sepenuhnya

Parsing Objek Bersarang

Kami telah memeriksa bagaimana Anda dapat menggunakan model data di dalam model lain, tetapi kadang-kadang perlu untuk menguraikan bidang JSON yang termasuk dalam bidang lain tanpa menggunakan model data terpisah. Masalahnya akan lebih jelas jika kita mempertimbangkannya dengan sebuah contoh. Kami memiliki JSON berikut:

 { "id": 349, "art": "M0470500", "title": "- Vichy 50 ", "ratings": { "average_rating": 4.1034, "votes_count": 29 } } 


Kita perlu menguraikan bidang "rata-rata" dan "suara_kount" , ini dapat diselesaikan dengan dua cara, baik membuat model data Peringkat dengan dua bidang dan menyimpan data ke dalamnya, atau Anda dapat menggunakan nestedContainer . Kami sudah membahas kasus pertama, dan penggunaan yang kedua akan terlihat seperti ini:

 class Product: Decodable { var id: Int var art: String? var title: String? var votesCount: Int var averageRating: Double enum CodingKeys: String, CodingKey { case id case art case title case ratings } enum RatingsCodingKeys: String, CodingKey { case votesCount = "votes_count" case averageRating = "average_rating" } required init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.id = try container.decode(Int.self, forKey: .id) self.art = try? container.decode(String.self, forKey: .art) self.title = try? container.decode(String.self, forKey: .title) // Nested ratings let ratingsContainer = try container.nestedContainer(keyedBy: RatingsCodingKeys.self, forKey: .ratings) self.votesCount = try ratingsContainer.decode(Int.self, forKey: .votesCount) self.averageRating = try ratingsContainer.decode(Double.self, forKey: .averageRating) } } 


Artinya, masalah ini diselesaikan dengan membuat wadah tambahan lain menggunakan nestedContainter dan penguraian selanjutnya. Opsi ini nyaman jika jumlah bidang bersarang tidak begitu besar, jika tidak lebih baik menggunakan model data tambahan.

Ketidakcocokan nama bidang JSON dan properti model data

Jika Anda memperhatikan bagaimana enumerasi didefinisikan dalam model data kami, Anda dapat melihat bahwa elemen enumerasi kadang-kadang diberikan string yang mengubah nilai default, misalnya:

 enum RatingsCodingKeys: String, CodingKey { case votesCount = "votes_count" case averageRating = "average_rating" } 


Ini dilakukan agar sesuai dengan nama-nama variabel model dan bidang JSON. Ini biasanya diperlukan untuk bidang yang namanya terdiri dari beberapa kata, dan di JSON mereka dipisahkan oleh garis bawah. Pada prinsipnya, pendefinisian ulang pencacahan adalah yang paling populer dan terlihat sederhana, tetapi bahkan saat itu Apple datang dengan solusi yang lebih elegan. Masalah ini dapat diselesaikan dalam satu baris menggunakan keyDecodingStrategy . Fitur ini muncul di Swift 4.1

Katakanlah Anda memiliki JSON formulir:

 let jsonString = """ [ { "name": "MacBook Pro", "screen_size": 15, "cpu_count": 4 }, { "name": "iMac Pro", "screen_size": 27, "cpu_count": 18 } ] """ let jsonData = Data(jsonString.utf8) 


Mari kita buat model data untuk itu:

 struct Mac: Codable { var name: String var screenSize: Int var cpuCount: Int } 

Variabel dalam model dicatat sesuai dengan perjanjian, mulai dengan huruf kecil dan kemudian setiap kata dimulai dengan huruf kapital (yang disebut camelCase ). Tetapi dalam JSON, bidang ditulis dengan garis bawah (yang disebut snake_case ). Sekarang, agar parsing berhasil, kita perlu mendefinisikan enumerasi dalam model data di mana kita akan menetapkan korespondensi nama-nama bidang JSON dengan nama-nama variabel, atau kita akan mendapatkan kesalahan runtime. Tapi sekarang mungkin untuk hanya mendefinisikan keyDecodingStrategy

 let decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase do { let macs = try decoder.decode([Mac].self, from: jsonData) } catch { print(error.localizedDescription) } 


Untuk fungsi penyandian , Anda dapat menggunakan transformasi terbalik:

 encoder.keyEncodingStrategy = .convertToSnakeCase 


Dimungkinkan juga untuk menyesuaikan keyDecodingStrategy menggunakan penutupan berikut:

 let jsonDecoder = JSONDecoder() jsonDecoder.keyDecodingStrategy = .custom { keys -> CodingKey in let key = keys.last!.stringValue.split(separator: "-").joined() return PersonKey(stringValue: String(key))! } 


Entri ini, misalnya, memungkinkan penggunaan pemisah "-" untuk JSON. Contoh JSON yang digunakan:

 { "first-Name": "Taylor", "last-Name": "Swift", "age": 28 } 

Dengan demikian, definisi tambahan enumerasi seringkali dapat dihindari.

Menangani kesalahan

Saat mem-parsing JSON dan mengonversi data dari satu format ke format lain, kesalahan tidak bisa dihindari, jadi mari kita lihat opsi untuk menangani berbagai jenis kesalahan. Saat mendekode, jenis kesalahan berikut dimungkinkan:

  • DecodingError.dataCorrupted (DecodingError.Context) - data rusak. Biasanya berarti bahwa data yang Anda coba dekode tidak cocok dengan format yang diharapkan, misalnya, alih-alih JSON yang diharapkan, Anda menerima format yang sama sekali berbeda.
  • DecodingError.keyNotFound (CodingKey, DecodingError.Context) - bidang yang diminta tidak ditemukan. Berarti bidang yang Anda harapkan diterima tidak ada
  • DecodingError.typeMismatch (Any.Type, DecodingError.Context) - ketik ketidakcocokan. Ketika tipe data dalam model tidak cocok dengan jenis bidang yang diterima
  • DecodingError.valueNotFound (Any.Type, DecodingError.Context) - nilai yang hilang untuk bidang tertentu. Bidang yang Anda tentukan dalam model data tidak dapat diinisialisasi, mungkin dalam data yang diterima bidang ini nihil. Kesalahan ini hanya terjadi pada bidang non-opsional, jika bidang tidak harus memiliki nilai, jangan lupa untuk menjadikannya opsional.


Saat menyandikan data, kesalahan mungkin terjadi:

EncodingError.invalidValue (Any.Type, DecodingError.Context) - gagal mengubah model data ke format tertentu

Contoh penanganan kesalahan saat mem-parsing JSON:

  do { let decoder = JSONDecoder() _ = try decoder.decode(businessReviewResponse.self, from: data) } catch DecodingError.dataCorrupted(let context) { print(DecodingError.dataCorrupted(context)) } catch DecodingError.keyNotFound(let key, let context) { print(DecodingError.keyNotFound(key,context)) } catch DecodingError.typeMismatch(let type, let context) { print(DecodingError.typeMismatch(type,context)) } catch DecodingError.valueNotFound(let value, let context) { print(DecodingError.valueNotFound(value,context)) } catch let error{ print(error) } 


Pemrosesan kesalahan tentu saja lebih baik untuk dimasukkan ke dalam fungsi yang terpisah, tetapi di sini, untuk kejelasan, analisis kesalahan dilakukan bersama dengan penguraian. Misalnya, output kesalahan jika tidak ada nilai untuk bidang "produk" akan terlihat seperti ini:

gambar

Perbandingan Codable dan NSCoding

Tentu saja, protokol Codable adalah langkah besar ke depan dalam pengodean / dekode data, tetapi protokol NSCoding ada sebelum itu. Mari kita coba membandingkannya dan melihat apa manfaat Codable:

  • Saat menggunakan protokol NSCoding , objek harus berupa subkelas dari NSObject , yang secara otomatis menyiratkan bahwa model data kami harus kelas. Dalam Codable , masing-masing tidak perlu pewarisan, model data dapat berupa kelas, dan struct dan enum .
  • Jika Anda memerlukan fungsi enkode dan dekode terpisah, seperti, misalnya, dalam penguraian data JSON yang diterima melalui API, Anda hanya dapat menggunakan satu protokol yang dapat didekodekan . Artinya, tidak perlu menerapkan init atau metode penyandian yang terkadang tidak perlu.
  • Codable dapat secara otomatis menghasilkan metode init dan enkode yang diperlukan, serta enumerasi CodingKeys opsional. Ini, tentu saja, hanya berfungsi jika Anda memiliki bidang sederhana dalam struktur data, jika tidak, penyesuaian tambahan akan diperlukan. Dalam kebanyakan kasus, terutama untuk struktur data dasar, Anda dapat menggunakan pembuatan otomatis, terutama jika Anda mendefinisikan kembali keyDecodingStrategy , ini nyaman dan mengurangi beberapa kode yang tidak perlu.


Protokol Codable, Decodable, dan Encodable memungkinkan kami untuk mengambil langkah lain menuju kenyamanan konversi data, alat parsing baru yang lebih fleksibel muncul, jumlah kode dikurangi, dan sebagian proses konversi diotomatiskan. Protokol secara asli diimplementasikan dalam Swift 4 dan memungkinkan untuk mengurangi penggunaan perpustakaan pihak ketiga, seperti SwiftyJSON , sambil mempertahankan kegunaan. Protokol juga memungkinkan untuk mengatur struktur kode dengan benar dengan memisahkan model data dan metode untuk bekerja dengannya menjadi modul terpisah.

Source: https://habr.com/ru/post/id414221/


All Articles