Deserialisasi JSON “antik” di Android


keyboard
https://unsplash.com/photos/WzKPT0IuUrU

Tak afdol rasanya jika aplikasi Android yang kita bangun tidak bisa berkomunikasi dengan dunia luar. Tidak online. Tidak terhubung ke awan. Nah, salah satu cara aplikasi berkomunikasi dengan dunia luar adalah melalui perantara API, entah itu lewat Restful maupun GraphQL. Sementara sebagian kecil masih setia dengan sabun soap.

Biasanya, web service semacam Restful ataupun GraphQL itu mengirimkan data tidak dalam bentuk objek, melainkan diserialisasi menjadi bentuk string agar mudah dikirim. Berkas string itu biasanya menggunakan format JSON saat melakukan komunikasi antara server dengan client, dalam hal ini aplikasi Android. Meski ada sebagian penyedia API yang masih menggunakan format XML (dan masih pakai sabun soap).

Sederhananya, alur data dari server ke client kira-kira seperti ini: Dari server data objek diubah (serialisasi) ke json -> dikirim lewat jaringan -> oleh aplikasi data json itu diubah lagi (deserialisasi) menjadi bentuk objek.

Misalkan kita ingin meminta data detail user yang isinya terdiri dari nama, userid, dan alamat ke server. Data json dari server misalnya seperti ini:

{
    "status": "OK",
    "response": {
      "userId": "abcdef1234",
      "name": "John snow",
      "age": 23,
      "address": [
        {
          "label": "home",
          "street": "Jalan kebahagiaan",
          "subdistrict": "gembor",
          "district": "tujuh obor",
          "city": "City of hope"
        },
        {
          "label": "office",
          "street": "Jalan raya di mana",
          "subdistrict": "gembir",
          "district": "asumsi",
          "city": "City of gold"
        }
      ]
    }
  }

Di sisi client, kita harus membuat data class untuk menampung data tersebut. Misal bentuknya seperti ini:

data class UserResponse(
    val status: String? = null,
    val response: User? = null
)

data class User(
    val name: String? = null,
    val userId: String? = null,
    val age: Int? = null,
    val addresses: List<UserAddress>? = null
)

data class UserAddress(
    val label: String? = null,
    val street: String? = null,
    val subdistrict: String? = null,
    val district: String? = null,
    val city: String? = null
)

Dengan bantuan Gson, kita dengan mudah mengubah data json di atas menggunakan data class di atas. Seperti magic, hanya dengan beberapa baris saja, json tersebut sudah dideserialisasi dan berubah bentuk menjadi sebuah objek User. Kira-kira kodenya seperti ini.

val user = Gson().fromJson(userJson, User::class.java)

Mudah bukan?

Masalahnya adalah, kadang kala json yang diterima sama client tidak seindah yang dibayangkan. Alias tidak sesuai standar yang berlaku pada umumnya di dunia Android. Misalnya bentuk data yang dikirim seperti ini.

   {
    "status": "OK",
    "response": {
      "user": {
        "userId": "abcdef1234",
        "name": "John snow",
        "age": 23,
        "address": [
          "home", "office"
        ]
      },
      "addressMap": {
        "home": {
          "label": "home",
          "street": "Jalan kebahagiaan",
          "subdistrict": "gembor",
          "district": "tujuh obor",
          "city": "City of hope"
        },
        "office": {
          "label": "office",
          "street": "Jalan raya di mana",
          "subdistrict": "gembir",
          "district": "asumsi",
          "city": "City of gold"
        },
        "apartment": {
          "label": "apartment",
          "street": "Jalan raya di sini",
          "subdistrict": "tirto",
          "district": "asumsi",
          "city": "City of diamond"
        },
        "kosan": {
          "label": "kosan",
          "street": "Jalan raya di sini",
          "subdistrict": "tirto",
          "district": "asumsi",
          "city": "City of diamond"
        }
      }
    }
  }

Perhatikan field address di dalam user kini tak lagi berisi objek address langsung, tapi hanya berupa label saja, yang merujuk pada objek addressMap di bawahnya. Selain itu, label home dan office kini menjadi key dari objek alamat, sesuatu yang bikin masalah tambah kompleks.

Untuk mengatasinya, ya mudah saja. Ganti saja responsenya menyesuaikan dengan skema json tersebut. Misalnya mengubah val addresses: List<address>? = null menjadi val addresses: List<String>? = null. (Sementara untuk yang objek address nanti akan dibahas di bawah).

Untuk membikin masalah semakin kompleks, ternyata kita tetap menginginkan hasil akhir objeknya tidak berubah:

data class User(
    val name: String? = null,
    val userId: String? = null,
    val age: Int? = null,
    val addresses: List<UserAddress>? = null
)

alias si variabel addresses harus tetap berupa list objek UserAddress alih-alih list string. Karena ternyata format data class tersebut sudah diimplementasikan di activity lain yang juga akan kita kirimi datanya. Sehingga kita tidak bisa mengubah class User seenaknya, karena ditakutkan akan menimbulkan regresi di activity lain.

Apakah ada solusinya? Tentu saja ada. Minimal ada dua cara.

Solusi pertama, kita tidak hanya mengandalkan Gson untuk mengubah json ke dalam bentuk objek. Kali ini kita butuh bantuan dari JSONObject. Namun sebelumnya, kita harus agak mengakali data class agar bisa menerima inputan json yang aneh tersebut. Maka, kita ubah class UserResponse dengan menambahkan objek baru, DataUser, dan membuat kelas baru untuk User, misalnya UserTemp.

data class UserTemp(
    val name: String? = null,
    val userId: String? = null,
    val age: Int? = null,
    val addresses: List<String>? = null
)

data class DataUser(
    val user: UserTemp? = null,
    val address: Any? = null
)

data class UserResponse(
    val status: String? = null,
    val response: DataUser? = null
)

Perhatikan di kelas DataUser, untuk variabel address, alih-alih menggunakan tipe data Address, kita menggunakan Any yang bersifat umum. Kita terpaksa menggunakan tipe data Any karena kita harus menangkap key objek json dari response API sebagai label di kelas UserAddress.

Sebenarnya, menggunakan tipe data Any di data class tidak disarankan karena bisa berpotensi salah casting, tapi karena bentuk json-nya memang “unik” ya mau apalagi kita. Jadi berhati-hatilah.

Saatnya untuk mapping data alamat dari DataUser ke dalam kelas User. Untuk field name, userId, dan age harusnya tidak ada masalah. Kita tinggal masukkan nilai dari UserTemp ke dalam User. Yang bermasalah adalah di field addresses. Kita harus mentransformasikan data dari address di DataUser ke dalam field addresses yang bertipe array dari UserAddress.

Ok, langkah pertama adalah mengubah objek address yang dinamis menjadi list address. Di sini kita butuh bantuan Gson dan JSONObject.

…
//kita inject gson
@Inject lateinit var gson: Gson
…

fun convertAddressMap(addressMap: Any?): List<UserAddress> {
    val userAddresList = mutableListOf<UserAddress>()
    val jsonAddress = gson.toJson(addressMap)
    val jsonObject = JSONObject(jsonAddress)
    jsonObject.keys().forEach {
        val userAddressString = jsonObject.getJSONObject(it).toString()
        userAddressList.add(gson.fromJson(userAddressString, UserAddress::class.java))
    }
    return userAddressList
}

Fungsi di atas mengubah semua object di addressMap menjadi berbentuk list dengan tipe data UserAddress. Sudah selesai? Tentu saja belum. Setelah sukses dikonversi, langkah kedua adalah memasukkan address yang cocok dengan data list address di kelas User tadi.

fun mappingUserAddress(userTemp: UserTemp, listAddress: List<UserAddress>): User {
    val user = User()
    //masukkan data userTemp ke user
    user.name = userTemp.name
    user.userId = userTemp.userId
    user.age = userTemp.age
    
    val userAddressListString = userTemp.adresses.orEmpty()
    val tempUserAddressList = mutableListOf<UserAddress>()
    userAddressListString.forEach { item -> 
        val address = listAddress.find { it.label == item }
        address?.let { tempUserAddressList.add(it) }
    }
    
    user.addresses = tempUserAddressList
    return user
}

Oke, fungsi-fungsi yang dibutuhkan sudah dibuat. Mari kita gunakan untuk menkonversi si json tersebut.

fun showDataUser(response: UserResponse) {
    val userAddressMap = response.address
    val listAddressMap = convertAddressMap(userAddressMap)
    val user = mappingUserAddress(response.user, listAddressMap)
}

Itulah cara yang pertama. Mudah sekali, ya, kelihatannya. Lalu bagaimana dengan cara yang kedua?

Cara yang kedua lebih mudah. yakni dengan meminta orang backend yang mengerjakan fitur ini untuk mengubah format json yang dikirimkan. Tentu saja jika langkah kedua ini ditempuh, tulisan ini tidak perlu dibuat~

*Tulisan ini terinspirasi dari kisah nyata, di mana data json-nya lebih kompleks dari contoh di atas.


Ada komentar?

This site uses Akismet to reduce spam. Learn how your comment data is processed.