Bab lalu kita sengaja melewati anotasi validasi di CreateOrderRequest supaya bisa fokus ke struktur DTO dulu. Sekarang saatnya membongkarnya tuntas — bagaimana server menolak request yang tidak masuk akal (quantity negatif, nama kosong) sebelum sempat menyentuh business logic sama sekali.
Jakarta Bean Validation
Spring Boot mengintegrasikan Jakarta Bean Validation (standar validasi berbasis anotasi di ekosistem Java/Jakarta EE, diaktifkan lewat dependency spring-boot-starter-validation di build.gradle.kts) langsung ke objek request. Idenya: alih-alih menulis if (quantity < 1) throw ... manual di setiap tempat yang menerima input, kita cukup mendeklarasikan aturan validnya lewat anotasi di field, dan framework yang menegakkannya secara otomatis.
Begini bentuknya di CreateOrderRequest:
data class CreateOrderRequest(
@field:NotBlank(message = "customerName tidak boleh kosong")
val customerName: String,
@field:NotBlank(message = "itemName tidak boleh kosong")
val itemName: String,
@field:Min(value = 1, message = "quantity minimal 1")
val quantity: Int,
@field:DecimalMin(value = "0.01", message = "unitPrice harus lebih dari 0")
val unitPrice: BigDecimal
)
Tiga anotasi yang dipakai di sini masing-masing menegakkan aturan berbeda:
@NotBlank— nilaiStringtidak bolehnull, tidak boleh string kosong (""), dan tidak boleh cuma berisi whitespace. Dipakai dicustomerNamedanitemName— dua field yang tidak masuk akal kalau kosong.@Min(value = 1)— nilai numerik (di siniInt) harus lebih besar atau sama dengan 1. Dipakai diquantity, karena order dengan jumlah 0 atau negatif tidak valid secara bisnis.@DecimalMin(value = "0.01")— mirip@Min, tapi untuk tipe desimal presisi tinggi sepertiBigDecimal. Perhatikan nilainya ditulis sebagaiString("0.01"), bukan literal angka — ini karenaBigDecimalbutuh presisi eksak yang tidak selalu bisa direpresentasikan aman lewat tipe primitif sepertiDouble.
Setiap anotasi juga membawa parameter message — teks yang akan dikembalikan ke client persis seperti yang kita tulis, kalau validasi field itu gagal. Ini yang membuat error response nanti informatif, bukan sekadar "400 Bad Request" tanpa penjelasan.
@Valid: Pemicu di Sisi Controller
Anotasi di CreateOrderRequest sendiri tidak berbuat apa-apa kalau tidak ada yang memicu pengecekannya. Pemicunya ada di OrderController, yang sudah kita lihat sekilas di bab routing:
@PostMapping
fun createOrder(@Valid @RequestBody request: CreateOrderRequest): ResponseEntity<OrderResponse> {
val order = orderService.createOrder(request)
return ResponseEntity.status(HttpStatus.CREATED).body(order.toResponse())
}
@Valid di depan @RequestBody request: CreateOrderRequest memberi tahu Spring: setelah body JSON berhasil di-deserialisasi jadi CreateOrderRequest, jalankan semua anotasi validasi yang ada di kelas itu sebelum method createOrder sempat dieksekusi. Kalau semua field valid, eksekusi lanjut normal — request yang diterima di dalam body method sudah dijamin memenuhi semua aturan yang kita tulis. Kalau ada satu saja field yang gagal validasi, Spring langsung melempar MethodArgumentNotValidException dan method createOrder tidak pernah dijalankan sama sekali — business logic di OrderService tidak pernah menerima data yang cacat.
Kenapa Butuh @field:? Gotcha Kotlin + Java Annotation
Ini bagian yang penting dipahami betul, karena kalau terlewat, hasilnya adalah bug yang diam-diam tidak muncul error apa pun — validasi kelihatannya terpasang, tapi tidak pernah benar-benar jalan.
Di Java, sebuah field pada dasarnya cuma satu hal: variabel di dalam kelas. Anotasi yang ditempel di sana jelas menempel ke field itu. Tapi di Kotlin, satu deklarasi seperti val customerName: String di primary constructor sebuah data class sebenarnya menghasilkan beberapa elemen sekaligus di level bytecode: parameter constructor, backing field privat, dan property dengan getter publik. Ketika kamu menulis anotasi tanpa penanda tambahan, Kotlin harus menebak: anotasi ini dimaksudkan untuk menempel ke yang mana?
Untuk sebagian besar anotasi yang didesain di Kotlin sendiri, tebakan itu jarang jadi masalah. Tapi library validasi seperti Jakarta Bean Validation didesain di dunia Java, dan cara kerjanya adalah membaca field lewat reflection. Kalau anotasi seperti @NotBlank menempel ke parameter constructor (perilaku default Kotlin untuk anotasi di posisi ini pada banyak kasus) alih-alih ke field, mesin validasi tidak akan pernah menemukannya saat scanning — validasi itu diam-diam tidak pernah berjalan, tanpa ada warning atau error yang memberi tahu kita ada yang salah.
Solusinya adalah use-site target — sintaks @field:NamaAnotasi(...) yang secara eksplisit memberi tahu compiler: "tempelkan anotasi ini ke field-nya, bukan ke parameter constructor atau property." Itu sebabnya setiap anotasi validasi di CreateOrderRequest konsisten ditulis dengan awalan @field: — bukan gaya penulisan opsional, tapi keharusan supaya Jakarta Bean Validation benar-benar bisa menemukan dan menegakkan aturannya. Ini salah satu gotcha paling umum saat pemrograman Kotlin bertemu dengan library Java berbasis anotasi dan reflection — kalau kamu pernah dengar keluhan "validasi Kotlin-ku kok tidak jalan padahal anotasinya sudah ada", akar masalahnya hampir selalu ini.
Apa yang Terjadi Saat Validasi Gagal?
Begitu ada field yang melanggar aturannya — misalnya request dengan quantity: 0 — Spring melempar MethodArgumentNotValidException. Kalau kita belum menangani exception ini secara eksplisit, Spring Boot akan mengembalikan halaman error default yang generik, tidak konsisten dengan format response lain di API kita. Bab berikutnya, bab penutup Fase 1, kita akan lihat bagaimana GlobalExceptionHandler menangkap exception ini (bersama beberapa exception lain) secara terpusat, mengubahnya jadi response error yang rapi dan seragam di seluruh Order Service.
Bagian dari Series: Kotlin Backend Microservice - Belajar Fundamental sampai Microservice Nyata
Belajar Kotlin + Spring Boot dari fundamental bahasa (null safety, OOP, coroutines) sampai membangun sistem microservice sungguhan - REST API, Postgre...
Lihat Series Lengkap