Swift codable merupakan salah satu protocol yang mungkin akan sering digunakan, karena mempermudah developer untuk mem-parsing JSON format yang umumnya merupakan standart baku return dari API server.
Codable sendiri merupakan type alias dari Encodable dan Decodable , yang berarti dapat meng-encode ataupun men-decode ‘dirinya’ menjadi bentuk lain tanpa perlu ada proses manual.
Anggap ada json yang berisi beberapa karakter game of thrones season akhir yang menjelaskan apakah karakter itu masih hidup dan nama aliasnya,
let json = """
{
"characters": [
{
"first_name": "Daenerys",
"last_name": "Targaryen",
"alias": "The Mother of Dragons",
"is_alive": 1
},
{
"first_name": "Cersei",
"last_name": "Lannister",
"alias": null,
"is_alive": 0
},
{
"first_name": "Jon",
"last_name": "Snow",
"alias": "The White Wolf",
"is_alive": "resurrected"
}
]
}
"""
Old fashioned way
Maka dahulu kala untuk memparsing dapat dilakukan dengan hal semacam berikut,
let jsonObj = Data(json.utf8)
if let objects = try JSONSerialization.jsonObject(with: jsonObj) as? [String: Any] {
if let characters = objects["characters"] as? [[String: Any]] {
for character in characters {
let name = character["first_name"] ?? ""
if let isAlive = character["is_alive"] as? Int {
print("\(name) is \(isAlive != 0 ? "alive" : "dead")")
} else if let other = character["is_alive"] as? String {
print("\(name) is \(other)")
}
}
}
}
Semua nya masih terbilang manual, dan banyak type casting disitu yang kalau json nya lebih panjang maka type casting nya pun jadi lebih panjang dan ngejelimet.
Output yang dihasilkan (spoiler alert)
Daenerys is alive
Cersei is dead
Jon is resurrected
SwiftyJSON (kind of) way
Atau kalau rajin sedikit bisa dengan membuat satu struct sendiri seperti berikut
struct RawJSON {
var value: Any?
init(json string: String) throws {
let data = Data(string.utf8)
value = try JSONSerialization.jsonObject(with: data, options: .allowFragments)
}
init(value: Any?) {
self.value = value
}
var string: String? {
value as? String
}
var int: Int? {
value as? Int
}
var array: [RawJSON]? {
let converted = value as? [Any]
return converted?.map{ RawJSON(value: $0) }
}
var dictionary: [String: RawJSON]? {
let converted = value as? [String: Any]
return converted?.mapValues { RawJSON(value: $0) }
}
subscript(key: String) -> RawJSON {
dictionary?[key] ?? RawJSON(value: nil)
}
}
Pemakaiannya berubah menjadi
let jsonRaw = try RawJSON(json: json)
jsonRaw["characters"].array?.forEach({ (json) in
if let isAlive = json["is_alive"].int {
print("\(json["first_name"].string ?? "") is \(isAlive != 0 ? "alive" : "dead")")
} else if let other = json["is_alive"].string {
print("\(json["first_name"].string ?? "") is \(other)")
}
})
Outputnya masih sama seperti metode sebelumnya.
Developer yang pernah bersinggungan dengan SwiftyJSON mungkin familiar dengan konsep diatas.
Sudah tidak diperlukan lagi type casting walaupun pada contoh diatas seluruh type yang keluar adalah optional, kalaupun optionalnya mau dihilangkan yang paling mudah dengan cara membuat satu variable non-optional yang membuka optional value dengan menyediakan default value sebagai fall back nya.
Kekurangannya adalah seluruh key masih perlu diinput manual dan literal, walaupun bisa disiasati dengan membuat semacam list ataupun enum untuk masing-masing key nya.
Codable Way
Jika menggunakan metode Codable, yang pertama harus dilakukan adalah membuat sebuah struct yang mengadopsi protocol Codable seperti berikut,
struct Characters: Codable {
let characters: [Detail]
struct Detail: Codable {
let firstName: String
let lastName: String
let alias: String
let isAlive: Int
enum CodingKeys: String, CodingKey {
case firstName = "first_name"
case lastName = "last_name"
case alias
case isAlive = "is_alive"
}
}
}
🤔️ terlihat lebih rapih, terstruktur dan mudah dibaca oleh orang lain, seluruh key nya pun bisa terlebih dahulu di definisi dengan CodingKey yang “ajaib”.
Cara penggunaannya pun cendrung lebih mudah, seperti berikut
let jsonObj = Data(json.utf8)
let decoded = try JSONDecoder().decode(Characters.self, from: jsonObj)
decoded.characters.forEach { (character) in
switch character.isAlive {
case .alive:
print("\(character.firstName) is alive")
case .dead:
print("\(character.firstName) is dead")
case .other(let status):
print("\(character.firstName) is \(status)")
}
}
Output masih sama seperti dua metode sebelumnya, tapi melihat sepintas metode dengan Codable ini sepertinya lebih deklaratif, lugas dan bisa dipahami secara sederhana.
Blocker
Tapi… dengan contoh JSON diatas,
Jika dijalankan maka terjadi error seperti berikut
hal ini disebabkan oleh alias
key yang ternyata bisa diisi oleh type String ataupun JSON null (nil).
Karena swift secara DNA sudah mengadopsi konsep optional value maka untuk mengatasi hal tersebut, alias: String
bisa dibuat menjadi optional sehingga struct Characters menjadi seperti berikut,
struct Characters: Codable {
let characters: [Character]
struct Character: Codable {
let firstName: String
let lastName: String
let alias: String?
let isAlive: Int
enum CodingKeys: String, CodingKey {
case firstName = "first_name"
case lastName = "last_name"
case alias
case isAlive = "is_alive"
}
}
}
Ok, coba jalankan lagi.
🤔️ ternyata masih ada error
Ternyata ini dikarenakan is_alive
yang valuenya bisa berupa Int tapi kadang juga bisa berupa String.
Dari sini terlihat kekurangan dari Codable itu sendiri, yaitu sebaiknya antara developer backend dan developer mobile harus memiliki contract API yang jelas diawal.
Tapi kadang, developer mobile tidak punya kuasa untuk mengatur developer backend 😅️.
Maka sebaiknya kita bisa berdamai dengan diri sendiri 👼️, dan salah satu alternatif yang mungkin bisa dilakukan ketika bertemu situasi seperti ini dengan memasukan value is_alive
kedalam sebuah enum yang pastinya juga mengadopsi protocol Codable.
Implementasinya kira-kira seperti ini,
struct Characters: Codable {
enum isAlive: Codable {
case alive
case dead
case other(String)
func encode(to encoder: Encoder) throws {
var sd = encoder.singleValueContainer()
switch self {
case .alive:
try sd.encode(1)
case .dead:
try sd.encode(0)
case .other(let str):
try sd.encode(str)
}
}
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let value = try? container.decode(Int.self) {
switch value {
case 0:
self = .dead
case 1:
self = .alive
default:
self = .other("Undefined")
}
} else if let value = try? container.decode(String.self) {
self = .other(value)
} else {
self = .other("Undefined")
}
}
}
let characters: [Details]
struct Details: Codable {
let firstName: String
let lastName: String
let alias: String?
let isAlive: isAlive
enum CodingKeys: String, CodingKey {
case firstName = "first_name"
case lastName = "last_name"
case alias
case isAlive = "is_alive"
}
}
}
Karena enum isAlive ini Codable, maka secara tidak langsung perlu adanya func yang bisa men-decode dan men-encode, karena seperti dibilang di atas, Codable sendiri adalah type alias dari Encodable dan Decodable.
enum isAlive ini ga harus berada di dalam struct Characters ya, tapi bisa dipisah.
Jika dipisah kira-kira akan jadi seperti ini,
enum isAlive: Decodable {
case alive
case dead
case other(String)
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let value = try? container.decode(Int.self) {
switch value {
case 0:
self = .dead
case 1:
self = .alive
default:
self = .other("Undefined")
}
} else if let value = try? container.decode(String.self) {
self = .other(value)
} else {
self = .other("Undefined")
}
}
}
extension isAlive: Encodable {
func encode(to encoder: Encoder) throws {
var sd = encoder.singleValueContainer()
switch self {
case .alive:
try sd.encode(1)
case .dead:
try sd.encode(0)
case .other(let str):
try sd.encode(str)
}
}
}
struct Characters: Codable {
let characters: [Details]
struct Details: Codable {
let firstName: String
let lastName: String
let alias: String?
let isAlive: isAlive
enum CodingKeys: String, CodingKey {
case firstName = "first_name"
case lastName = "last_name"
case alias
case isAlive = "is_alive"
}
}
}
Jadi lebih jelas ya pembagiannya, mana fungsi yang men-decode dan mana yang men-encode.
Untuk merubah dari data ke json string juga bisa, cara penggunaanya seperti berikut,
let budi = Characters(characters: [Characters.Details(firstName: "Budi", lastName: "Ono", alias: "King of the King", isAlive: .other("not exist"))])
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let jsonData = try encoder.encode(budi)
let jsonString = String(data: jsonData, encoding: .utf8)
print(jsonString!)
Yang akan menghasilkan output
{
"characters" : [
{
"alias" : "King of the King",
"last_name" : "Ono",
"is_alive" : "not exist",
"first_name" : "Budi"
}
]
}