Mengonversi objek Kotlin ke JSON secara manual lewat string interpolation adalah sumber bug yang sering muncul: kutipan yang tidak di-escape, angka yang salah dikonversi, dan field yang terlupa. Plugin ContentNegotiation Ktor mengatasi hal tersebut dengan mendelegasikan serialisasi ke serializer yang terdaftar per tipe. Pada chapter ini kita akan memahami cara kerja ContentNegotiation dan memodelkannya dalam program Kotlin yang dapat dijalankan tanpa dependensi eksternal.
Plugin ContentNegotiation #
ContentNegotiation adalah plugin Ktor yang ditempatkan pada fase Content pipeline. Saat handler memanggil call.respond(objek), plugin mencari serializer terdaftar untuk tipe objek tersebut. Jika ditemukan, plugin mengonversi objek ke JSON (atau format lain seperti XML/CBOR), mengatur header Content-Type: application/json, dan menulis body. Sebaliknya, saat handler memanggil call.receive<T>(), plugin membaca body request dan mengonversi JSON kembali ke objek T.
Pada kode produksi, pemasangan plugin terlihat seperti berikut:
install(ContentNegotiation) {
json(Json {
ignoreUnknownKeys = true
prettyPrint = false
})
}Blok json { } adalah konfigurasi khusus untuk serializer JSON. Ktor mendukung beberapa serializer: kotlinx.serialization, Gson, dan Jackson. kotlinx.serialization adalah pilihan default karena native Kotlin dan menghasilkan kode compile-time safe.
Serializer per Tipe #
ContentNegotiation memelihara registry yang memetakan tipe Kotlin ke serializer. Saat call.respond(Pengguna(1, "Budi", "[email protected]")) dipanggil, plugin mencari JsonSerializer<Pengguna> di registry. Jika tidak ada, plugin melempar IllegalStateException.
Registry ini diisi otomatis oleh kompilator kotlinx.serialization saat anotasi @Serializable ditemukan di class. Kompilator menghasilkan Pengguna$$serializer yang dipanggil oleh plugin — proses ini transparan bagi pengembang.
Demo ContentNegotiation #
Karena kotlinx.serialization nyata membutuhkan kompilator plugin, kita akan memodelkan ContentNegotiation sebagai class Kotlin biasa dengan registry manual. Program ini mendaftarkan PenggunaSerializer dan kemudian memakainya untuk serialize/deserialize.
// ktor_content_negotiation.kt
// Mendemonstrasikan plugin ContentNegotiation pada Ktor yang mengonversi
// objek Kotlin <-> JSON otomatis. Pada kode produksi, plugin memakai
// kotlinx.serialization atau Gson. Di sini kita memakai parser JSON manual.
import kotlin.reflect.KClass
// Antarmuka serialisasi (mirip kotlinx.serialization KSerializer)
interface JsonSerializer<T> {
fun serialize(value: T): String
fun deserialize(text: String): T
}
// Mock data class
data class Pengguna(val id: Int, val nama: String, val email: String)
// Serializer sederhana untuk Pengguna
object PenggunaSerializer : JsonSerializer<Pengguna> {
override fun serialize(value: Pengguna): String {
return """{"id":${value.id},"nama":"${value.nama}","email":"${value.email}"}"""
}
override fun deserialize(text: String): Pengguna {
// Parser regex sederhana untuk demo
val idMatch = Regex(""""id":(\d+)""").find(text)?.groupValues?.get(1)?.toInt() ?: 0
val namaMatch = Regex(""""nama":"([^"]+)"""").find(text)?.groupValues?.get(1) ?: ""
val emailMatch = Regex(""""email":"([^"]+)"""").find(text)?.groupValues?.get(1) ?: ""
return Pengguna(idMatch, namaMatch, emailMatch)
}
}
// Plugin ContentNegotiation mendaftarkan serializer per tipe
class ContentNegotiation {
private val serializers = mutableMapOf<KClass<*>, JsonSerializer<*>>()
fun <T : Any> register(kclass: KClass<T>, serializer: JsonSerializer<T>) {
serializers[kclass] = serializer
}
inline fun <reified T : Any> register(serializer: JsonSerializer<T>) {
register(T::class, serializer)
}
@Suppress("UNCHECKED_CAST")
fun <T : Any> serialize(value: T): String {
val serializer = serializers[value::class] as? JsonSerializer<T>
?: throw IllegalStateException("Tidak ada serializer untuk ${value::class}")
return serializer.serialize(value)
}
@Suppress("UNCHECKED_CAST")
fun <T : Any> deserialize(text: String, kclass: KClass<T>): T {
val serializer = serializers[kclass] as? JsonSerializer<T>
?: throw IllegalStateException("Tidak ada serializer untuk $kclass")
return serializer.deserialize(text)
}
}
// Simulasi ApplicationCall.respond(value)
fun ApplicationCall.respondJson(value: Any, negotiation: ContentNegotiation) {
val json = negotiation.serialize(value)
responseBody = json
responseHeaders["Content-Type"] = "application/json"
}
fun ApplicationCall.receiveJson(negotiation: ContentNegotiation, kclass: KClass<*>): Any {
return negotiation.deserialize(requestBody, kclass)
}
class ApplicationCall(
val method: String = "GET",
val path: String = "/",
val requestBody: String = ""
) {
var responseStatus: Int = 200
var responseHeaders: MutableMap<String, String> = mutableMapOf()
var responseBody: String = ""
}
fun main() {
val negotiation = ContentNegotiation()
negotiation.register<Pengguna>(PenggunaSerializer)
println("=== Serialize Pengguna ke JSON ===")
val budi = Pengguna(1, "Budi", "[email protected]")
val call1 = ApplicationCall()
call1.respondJson(budi, negotiation)
println("Body: ${call1.responseBody}")
println("Content-Type: ${call1.responseHeaders["Content-Type"]}")
println("\n=== Deserialize JSON ke Pengguna ===")
val json = """{"id":2,"nama":"Siti","email":"[email protected]"}"""
val call2 = ApplicationCall(method = "POST", path = "/users", requestBody = json)
val received: Pengguna = call2.receiveJson(negotiation, Pengguna::class) as Pengguna
println("Received: $received")
println("Nama: ${received.nama}, Email: ${received.email}")
}Output yang Dihasilkan:
=== Serialize Pengguna ke JSON ===
Body: {"id":1,"nama":"Budi","email":"[email protected]"}
Content-Type: application/json
=== Deserialize JSON ke Pengguna ===
Received: Pengguna(id=2, nama=Siti, [email protected])
Nama: Siti, Email: [email protected]Mari kita bedah bagian penting kode di atas:
interface JsonSerializer<T>: kontrak serializer dengan dua method:serialize(objek ke JSON) dandeserialize(JSON ke objek). GenericTmemungkinkan satu interface untuk banyak tipe.object PenggunaSerializer : JsonSerializer<Pengguna>: implementasi konkret untukPengguna. Menggunakan regex untuk parsing JSON — pada produksi,kotlinx.serializationmenghasilkan parser yang efisien dan benar secara format.inline fun <reified T : Any> register(serializer): mendaftar serializer dengan tipeTyang ditentukan kompilator.reifiedmemungkinkan aksesT::classpada runtime — fitur yang dipakaikotlinx.serializationuntuk mencari serializer.fun serialize(value: T): String: lookup serializer berdasarkanvalue::class. Bila tidak ada, lemparIllegalStateExceptiondengan pesan yang menjelaskan tipe yang hilang.call2.receiveJson(negotiation, Pengguna::class): simulasicall.receive<Pengguna>(). Pada Ktor nyata,reifiedmembuat pemanggilan tidak perluPengguna::classeksplisit.
Content-Type Negotiation #
Sesuai namanya, ContentNegotiation sebenarnya bernegosiasi dengan klien berdasarkan header Accept dan Content-Type. Jika klien mengirim Accept: application/xml, plugin akan memilih serializer XML (jika terdaftar). Untuk sebagian besar API modern, JSON adalah format tunggal, sehingga negosiasi terlihat sederhana — tetapi arsitekturnya tetap mendukung multi-format.
Referensi Lanjutan #
- Ktor content negotiation
— panduan resmi plugin
ContentNegotiationuntuk serialisasi JSON otomatis di Ktor - Ktor serialization
— panduan resmi integrasi
kotlinx.serializationdengan Ktor server
Selanjutnya #
Setelah memahami mekanisme ContentNegotiation, langkah berikutnya adalah memahami sisi serializer itu sendiri. Chapter Ktor Serialization akan membahas kotlinx.serialization, anotasi @Serializable, dan bagaimana kompilator menghasilkan serializer otomatis.