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:

text Dua gaya balasan untuk payload yang sama
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:

java Validasi beranotasi dengan Jakarta Validation 3.1
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:

text
== 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:

  1. Baris 6 dan 8 (dua record): PermintaanProduk adalah payload, Kesalahan adalah unit laporan. Memisahkan keduanya sebagai record membuat daftar kesalahan bisa diiterasi, diurutkan, dan diubah ke bentuk apa pun — layar, JSON, log.
  2. Baris 12-16 (pemeriksaan nama): isBlank() lebih ketat daripada isEmpty() karena menangkap spasi dan tab — string " " gagal dengan benar. Rantai if/else if memastikan satu field hanya melaporkan satu pesan, bukan dua kesalahan yang saling tumpang tindih.
  3. 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.
  4. Baris 20-25 (pemeriksaan numerik): batas atas 10_000 mencegah stok mustahil; batas bawah 0 menolak nilai negatif. Output membuktikan keempat field bermasalah dilaporkan sekaligus, bukan hanya yang pertama.
  5. Baris 29-42 (jsonRespons): daftar kesalahan dirakit menjadi JSON dengan koma pemisah antar elemen — pola if (i > 0) b.append(",") yang sama dengan builder JSON pada Chapter Membangun REST API.
  6. Baris 55-57 (cetak kesalahan): format %-6s meratakan 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 berkas
java
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)));
    }
}