API yang kita bangun pada chapter sebelumnya menerima body JSON apa adanya: nama kosong, harga negatif, stok dua puluh ribu — semuanya masuk penyimpanan tanpa perlawanan. Data yang datang dari dunia luar tidak boleh dipercaya begitu saja; kesalahan klien, kecelakaan integrasi, hingga permintaan yang disusun dengan niat buruk semuanya bertemu di titik yang sama: batas API. Validasi adalah gerbang yang memeriksa payload sebelum satu baris logika bisnis dijalankan. Bab ini membahas prinsip validasi di batas (edge validation), format balasan kesalahan yang terstruktur, standar Jakarta Validation beranotasi, dan membuktikan polanya dengan validator manual berjalan di Java murni.
Validasi di Batas #
Aturan pertamanya tegas: validasi terjadi di batas, bukan di dalam. Method internal simpanProduk(nama, harga) berhak mengasumsikan data sudah bersih karena dipanggil sesudah gerbang; tanggung jawab pemeriksaan ada pada lapisan yang menerima input dari luar — endpoint API. Menempatkan pemeriksaan di batas membuat aturan terlihat sekali di satu tempat, bukan tersebar sebagai if defensif di setiap lapisan (pola yang sudah kita diskusikan sebagai defensive programming pada Section software design).
Aturan kedua: tolak dengan balasan yang membantu. Status 400 Bad Request disertai daftar kesalahan per field memungkinkan klien — atau pengembang klien — memperbaiki payload tanpa menebak. Bandingkan dua balasan ini:
Gaya buruk : 400 {"error": "payload tidak valid"}
Gaya baik : 400 {"status":"tidak valid","kesalahan":[
{"bidang":"nama","pesan":"wajib diisi"},
{"bidang":"harga","pesan":"harus bilangan bulat positif"}]}Kumpulkan Semua Kesalahan #
Dua strategi populer saat payload bermasalah: fail fast berhenti pada kesalahan pertama, sedangkan collect-all memeriksa seluruh field lalu melaporkan semuanya. Untuk API publik, collect-all jauh lebih ramah: klien memperbaiki empat field dalam satu iterasi alih-alih empat kali kirim-terima. Strategi ini menuntut struktur pengembalian berupa daftar (field, pesan), bukan satu string galat.
Standar Java untuk pekerjaan ini adalah Jakarta Validation. Aturan dinyatakan sebagai anotasi langsung pada field atau record component, lalu container atau framework menjalankannya otomatis:
public record PermintaanProduk(
@NotBlank(message = "wajib diisi")
@Size(max = 60)
String nama,
@Email
String email,
@Positive
int harga,
@Min(0) @Max(10_000)
int stok) {
}
@POST
public Response tambah(@Valid PermintaanProduk p) { // @Valid memicu pemeriksaan
...
}Anotasi @Valid pada parameter endpoint memicu pemeriksaan sebelum method dijalankan; implementasi standarnya adalah Hibernate Validator. Seluruh anotasi di atas — @NotBlank, @Email, @Positive, @Min, @Max — tinggal memetakan aturan yang akan kita tulis manual sebentar.
Validator Manual dalam Java Murni #
Lingkungan belajar kita tidak menjalankan container, jadi kita tulis gerbangnya sendiri. ValidasiPayload memeriksa empat field dengan strategi collect-all, lalu merakit dua bentuk balasan: daftar kesalahan untuk dibaca manusia dan JSON terstruktur untuk dikirim klien:
import java.util.ArrayList;
import java.util.List;
public class ValidasiPayload {
record PermintaanProduk(String nama, String email, int harga, int stok) {}
record Kesalahan(String bidang, String pesan) {}
static List<Kesalahan> periksa(PermintaanProduk p) {
List<Kesalahan> galat = new ArrayList<>();
if (p.nama() == null || p.nama().isBlank()) {
galat.add(new Kesalahan("nama", "wajib diisi"));
} else if (p.nama().length() > 60) {
galat.add(new Kesalahan("nama", "panjang maksimal 60 karakter"));
}
if (p.email() == null || !p.email().matches("[\\w.+-]+@[\\w-]+(\\.[\\w-]+)+")) {
galat.add(new Kesalahan("email", "format alamat email tidak valid"));
}
if (p.harga() <= 0) {
galat.add(new Kesalahan("harga", "harus bilangan bulat positif"));
}
if (p.stok() < 0 || p.stok() > 10_000) {
galat.add(new Kesalahan("stok", "harus di antara 0 dan 10000"));
}
return galat;
}
static String jsonRespons(List<Kesalahan> galat) {
if (galat.isEmpty()) {
return "{\"status\":\"valid\",\"data\":\"diproses\"}";
}
StringBuilder b = new StringBuilder("{\"status\":\"tidak valid\",\"kesalahan\":[");
for (int i = 0; i < galat.size(); i++) {
if (i > 0) {
b.append(",");
}
b.append("{\"bidang\":\"").append(galat.get(i).bidang())
.append("\",\"pesan\":\"").append(galat.get(i).pesan()).append("\"}");
}
return b.append("]}").toString();
}
public static void main(String[] args) {
PermintaanProduk sehat = new PermintaanProduk(
"Buku Java", "[email protected]", 85000, 10);
PermintaanProduk rusak = new PermintaanProduk(
"", "budi#example", -5000, 20000);
System.out.println("== Payload sehat ==");
System.out.println(jsonRespons(periksa(sehat)));
System.out.println();
System.out.println("== Payload bermasalah: kumpulkan semua kesalahan ==");
for (Kesalahan k : periksa(rusak)) {
System.out.printf(" %-6s %s%n", k.bidang(), k.pesan());
}
System.out.println();
System.out.println("== Respons 400 dengan daftar kesalahan ==");
System.out.println(jsonRespons(periksa(rusak)));
}
}Output yang Dihasilkan:
== Payload sehat ==
{"status":"valid","data":"diproses"}
== Payload bermasalah: kumpulkan semua kesalahan ==
nama wajib diisi
email format alamat email tidak valid
harga harus bilangan bulat positif
stok harus di antara 0 dan 10000
== Respons 400 dengan daftar kesalahan ==
{"status":"tidak valid","kesalahan":[{"bidang":"nama","pesan":"wajib diisi"},{"bidang":"email","pesan":"format alamat email tidak valid"},{"bidang":"harga","pesan":"harus bilangan bulat positif"},{"bidang":"stok","pesan":"harus di antara 0 dan 10000"}]}Bedah baris-per-baris validator ini:
- Baris 6 dan 8 (dua record):
PermintaanProdukadalah payload,Kesalahanadalah unit laporan. Memisahkan keduanya sebagai record membuat daftar kesalahan bisa diiterasi, diurutkan, dan diubah ke bentuk apa pun — layar, JSON, log. - Baris 12-16 (pemeriksaan
nama):isBlank()lebih ketat daripadaisEmpty()karena menangkap spasi dan tab — string" "gagal dengan benar. Rantaiif/else ifmemastikan satu field hanya melaporkan satu pesan, bukan dua kesalahan yang saling tumpang tindih. - Baris 17-19 (pemeriksaan
email): regex sengaja sederhana untuk keperluan pembelajaran. Validasi email yang teliti tetap tidak menjamin alamat itu ada — verifikasi sungguhan selalu lewat surel konfirmasi. - Baris 20-25 (pemeriksaan numerik): batas atas
10_000mencegah stok mustahil; batas bawah0menolak nilai negatif. Output membuktikan keempat field bermasalah dilaporkan sekaligus, bukan hanya yang pertama. - Baris 29-42 (
jsonRespons): daftar kesalahan dirakit menjadi JSON dengan koma pemisah antar elemen — polaif (i > 0) b.append(",")yang sama dengan builder JSON pada Chapter Membangun REST API. - Baris 55-57 (cetak kesalahan): format
%-6smeratakan kolom field agar manusia dapat memindai daftar dengan cepat — laporan validasi dibaca dua pihak, program dan manusia.
Payload yang sehat melaju ke logika bisnis; payload yang bermasalah kini ditolak dengan pesan terstruktur. Namun 400 hanyalah satu dari belasan cara permintaan gagal — resource hilang (404), konflik data (409), hingga kegagalan tak terduga (500). Menyatukan seluruh bentuk kegagalan itu dalam satu format balasan yang konsisten adalah pekerjaan Chapter Error Handling API.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
ValidasiPayload.java
Unduh berkasimport java.util.ArrayList;
import java.util.List;
public class ValidasiPayload {
record PermintaanProduk(String nama, String email, int harga, int stok) {}
record Kesalahan(String bidang, String pesan) {}
static List<Kesalahan> periksa(PermintaanProduk p) {
List<Kesalahan> galat = new ArrayList<>();
if (p.nama() == null || p.nama().isBlank()) {
galat.add(new Kesalahan("nama", "wajib diisi"));
} else if (p.nama().length() > 60) {
galat.add(new Kesalahan("nama", "panjang maksimal 60 karakter"));
}
if (p.email() == null || !p.email().matches("[\\w.+-]+@[\\w-]+(\\.[\\w-]+)+")) {
galat.add(new Kesalahan("email", "format alamat email tidak valid"));
}
if (p.harga() <= 0) {
galat.add(new Kesalahan("harga", "harus bilangan bulat positif"));
}
if (p.stok() < 0 || p.stok() > 10_000) {
galat.add(new Kesalahan("stok", "harus di antara 0 dan 10000"));
}
return galat;
}
static String jsonRespons(List<Kesalahan> galat) {
if (galat.isEmpty()) {
return "{\"status\":\"valid\",\"data\":\"diproses\"}";
}
StringBuilder b = new StringBuilder("{\"status\":\"tidak valid\",\"kesalahan\":[");
for (int i = 0; i < galat.size(); i++) {
if (i > 0) {
b.append(",");
}
b.append("{\"bidang\":\"").append(galat.get(i).bidang())
.append("\",\"pesan\":\"").append(galat.get(i).pesan()).append("\"}");
}
return b.append("]}").toString();
}
public static void main(String[] args) {
PermintaanProduk sehat = new PermintaanProduk(
"Buku Java", "[email protected]", 85000, 10);
PermintaanProduk rusak = new PermintaanProduk(
"", "budi#example", -5000, 20000);
System.out.println("== Payload sehat ==");
System.out.println(jsonRespons(periksa(sehat)));
System.out.println();
System.out.println("== Payload bermasalah: kumpulkan semua kesalahan ==");
for (Kesalahan k : periksa(rusak)) {
System.out.printf(" %-6s %s%n", k.bidang(), k.pesan());
}
System.out.println();
System.out.println("== Respons 400 dengan daftar kesalahan ==");
System.out.println(jsonRespons(periksa(rusak)));
}
}