Zod Dari Dasar Hingga Mahir: Panduan Lengkap Semua Jenis Validasi
Validasi adalah fondasi dari sistem yang sehat. Tanpa validasi yang baik, data yang masuk ke sistem bisa menyebabkan bug, inkonsistensi, bahkan celah keamanan. Zod adalah schema validation library berbasis TypeScript-first yang memungkinkan kamu mendefinisikan struktur data sekaligus mendapatkan type inference secara otomatis. Artikel ini membahas Zod secara bertahap, mulai dari primitive type sederhana hingga fitur lanjutan seperti discriminated union, transform, dan branding, lengkap dengan contoh kode yang bisa langsung dipakai.
Apa Itu Zod?
Zod adalah library yang dirancang untuk tiga hal utama: mendefinisikan schema data, melakukan validasi runtime, dan menghasilkan type TypeScript secara otomatis dari schema yang sudah didefinisikan. Karena schema dan type berasal dari sumber yang sama, kamu tidak perlu menulis interface TypeScript secara manual lalu menjaganya tetap sinkron dengan logic validasi — keduanya selalu konsisten.
Contoh paling sederhana:
import { z } from "zod";
const schema = z.string();
schema.parse("hello"); // valid
schema.parse(123); // melempar error
Jika data yang divalidasi tidak sesuai schema, Zod akan melempar error secara default. Di bagian selanjutnya kamu akan melihat cara menangani error tanpa exception menggunakan safeParse.
Instalasi
Zod diinstal seperti package npm biasa:
npm install zod
Tidak ada dependency tambahan yang diperlukan, dan Zod bisa langsung dipakai di project Node.js maupun di sisi frontend (misalnya untuk validasi form).
Primitive Types
Validasi dimulai dari tipe data paling dasar. Setiap primitive type di Zod punya method tambahan (chained method) untuk memperketat aturan validasi.
String
z.string();
Validasi tambahan yang sering dipakai:
z.string().min(3);
z.string().max(10);
z.string().length(5);
z.string().email();
z.string().url();
z.string().uuid();
z.string().regex(/^[A-Z]+$/);
z.string().startsWith("A");
z.string().endsWith("Z");
Number
z.number();
Validasi tambahan untuk number:
z.number().min(1);
z.number().max(100);
z.number().int();
z.number().positive();
z.number().negative();
z.number().nonnegative();
z.number().multipleOf(5);
Boolean
z.boolean();
Date
z.date();
Date juga bisa diberi batasan minimum dan maksimum:
z.date().min(new Date("2024-01-01"));
z.date().max(new Date());
Object Schema
Object schema adalah bentuk paling umum dipakai, karena hampir semua payload API berbentuk object dengan beberapa field.
const userSchema = z.object({
name: z.string(),
age: z.number(),
});
Salah satu keunggulan utama Zod adalah kemampuan menghasilkan type TypeScript langsung dari schema menggunakan z.infer:
type User = z.infer<typeof userSchema>;
Dengan ini, schema validasi menjadi single source of truth — kamu tidak perlu mendefinisikan type User secara terpisah dan berisiko tidak sinkron dengan aturan validasi yang sebenarnya.
Optional, Nullable, dan Default Value
Tidak semua field bersifat wajib. Zod menyediakan beberapa modifier untuk menangani field yang opsional, boleh null, atau punya nilai default.
Optional
Field boleh tidak ada (undefined):
z.string().optional();
Nullable
Field boleh bernilai null:
z.string().nullable();
Default Value
Field otomatis diisi nilai default jika tidak diberikan:
z.string().default("anonymous");
Ketiga modifier ini sering dikombinasikan sesuai kebutuhan, misalnya field yang opsional sekaligus punya default value ketika tidak diisi.
Array Validation
Untuk memvalidasi list data, gunakan z.array() dengan schema item di dalamnya:
z.array(z.string());
Array juga bisa diberi constraint tambahan terkait jumlah elemen:
z.array(z.string()).min(1);
z.array(z.string()).max(5);
z.array(z.number()).nonempty();
nonempty() memastikan array tidak boleh kosong — berguna untuk kasus seperti daftar item pesanan yang minimal harus berisi satu item.
Enum dan Literal
Untuk nilai yang terbatas dan terdefinisi, Zod menyediakan literal dan enum.
Literal
Literal memvalidasi bahwa nilai harus tepat sama dengan satu nilai tertentu:
z.literal("admin");
Enum
Enum memvalidasi bahwa nilai harus salah satu dari daftar yang ditentukan:
z.enum(["admin", "user", "guest"]);
Pendekatan ini cocok dipasangkan dengan konsep enum di level database — schema Zod menjadi lapisan validasi pertama sebelum data tersebut diproses lebih lanjut oleh aplikasi.
Union dan Discriminated Union
Kadang sebuah field bisa menerima lebih dari satu tipe data, atau struktur object berubah bergantung pada nilai field tertentu. Untuk kasus ini, Zod menyediakan union dan discriminatedUnion.
Union
Union memvalidasi bahwa nilai harus cocok dengan salah satu dari beberapa schema:
z.union([z.string(), z.number()]);
Discriminated Union
Discriminated union digunakan ketika struktur object berubah berdasarkan satu field “discriminator”:
z.discriminatedUnion("type", [
z.object({ type: z.literal("a"), value: z.string() }),
z.object({ type: z.literal("b"), value: z.number() }),
]);
Diagram berikut menggambarkan bagaimana Zod memutuskan schema mana yang dipakai berdasarkan nilai field type:
flowchart TD
A[Data masuk] --> B{Nilai field type?}
B -- "a" --> C[Validasi sebagai schema type a, value: string]
B -- "b" --> D[Validasi sebagai schema type b, value: number]
B -- lainnya --> E[Error: tidak ada schema yang cocok]Dibandingkan union biasa, discriminatedUnion memberikan error message yang lebih jelas dan performa validasi yang lebih baik, karena Zod tidak perlu mencoba semua kemungkinan schema satu per satu.
Nested Object
Object schema bisa disusun secara bertingkat untuk merepresentasikan struktur data yang lebih kompleks:
const schema = z.object({
user: z.object({
name: z.string(),
address: z.object({
city: z.string(),
}),
}),
});
Setiap level nested object tetap mendapatkan validasi dan type inference yang sama seperti object schema biasa.
Record (Dynamic Key Object)
Jika struktur object memiliki key yang dinamis — misalnya object yang key-nya berupa ID atau kode yang tidak diketahui sebelumnya — gunakan z.record():
z.record(z.string());
Kamu juga bisa menentukan tipe untuk key dan value secara terpisah:
z.record(z.string(), z.number());
Pola ini cocok untuk kasus seperti mapping { [productId: string]: number } yang merepresentasikan jumlah stok per produk.
Refinement (Custom Validation)
Tidak semua aturan validasi bisa diekspresikan dengan method built-in. Untuk logic validasi custom, Zod menyediakan refine() dan superRefine().
refine()
refine() cocok untuk validasi sederhana pada satu field:
z.string().refine((val) => val.includes("@"), {
message: "Harus mengandung @",
});
superRefine()
superRefine() cocok untuk validasi yang melibatkan beberapa field sekaligus, misalnya memastikan dua field saling cocok:
z.object({
password: z.string(),
confirmPassword: z.string(),
}).superRefine((data, ctx) => {
if (data.password !== data.confirmPassword) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Password tidak sama",
path: ["confirmPassword"],
});
}
});
superRefine memberikan akses ke ctx, sehingga kamu bisa menambahkan error pada field tertentu (path) dengan pesan yang spesifik.
Transform dan Preprocess
Selain memvalidasi, Zod juga bisa mengubah data sebelum atau sesudah validasi.
Transform
transform() mengubah data setelah validasi berhasil:
z.string().transform((val) => val.trim());
Contoh lain, mengubah string menjadi number setelah divalidasi sebagai string:
z.string().transform((val) => Number(val));
Preprocess
preprocess() memodifikasi input sebelum validasi dijalankan:
z.preprocess((val) => Number(val), z.number());
Pola ini sangat cocok untuk form input HTML, di mana semua input pada dasarnya berupa string meskipun secara semantik seharusnya berupa angka.
Intersection
Untuk menggabungkan dua schema menjadi satu, gunakan z.intersection():
const a = z.object({ name: z.string() });
const b = z.object({ age: z.number() });
const merged = z.intersection(a, b);
Hasil merged akan memvalidasi object yang harus memenuhi kedua schema sekaligus — memiliki field name bertipe string dan field age bertipe number.
Partial, Pick, Omit, dan Extend
Schema object yang sudah didefinisikan bisa dimodifikasi tanpa menulis ulang dari awal, menggunakan beberapa method bawaan.
Partial
Membuat semua field menjadi opsional:
userSchema.partial();
Pick
Mengambil sebagian field saja:
userSchema.pick({ name: true });
Omit
Menghilangkan field tertentu:
userSchema.omit({ age: true });
Extend
Menambahkan field baru ke schema yang sudah ada:
userSchema.extend({ role: z.string() });
Method-method ini sangat membantu untuk menjaga konsistensi antar schema yang saling berhubungan, misalnya schema untuk membuat user (tanpa id) dan schema untuk update user (semua field opsional).
Strict vs Passthrough
Secara default, Zod mengabaikan property tambahan yang tidak didefinisikan di schema. Perilaku ini bisa diubah dengan strict() atau passthrough().
Strict
Menolak property tambahan — validasi gagal jika ada field yang tidak dikenal:
z.object({ name: z.string() }).strict();
Passthrough
Mengizinkan property tambahan untuk tetap ikut di hasil validasi:
z.object({ name: z.string() }).passthrough();
Pilih strict() ketika kamu ingin memastikan payload benar-benar sesuai kontrak, misalnya pada endpoint API publik. Gunakan passthrough() ketika field tambahan memang diharapkan, misalnya saat schema hanya memvalidasi sebagian dari object yang lebih besar.
Safe Parse
Method parse() melempar error ketika validasi gagal, yang berarti kamu harus membungkusnya dengan try/catch. Sebagai alternatif, safeParse() selalu mengembalikan object hasil tanpa melempar exception:
const result = schema.safeParse(data);
if (!result.success) {
console.log(result.error);
}
// ANTI-PATTERN: parse() di boundary layer tanpa try/catch bisa membuat request crash
const data = schema.parse(input);
// BENAR: safeParse() di boundary layer, error ditangani secara eksplisit
const result = schema.safeParse(input);
if (!result.success) {
return { status: 400, error: result.error.format() };
}
const data = result.data;
safeParse() lebih cocok dipakai di boundary layer seperti controller API atau service, di mana kegagalan validasi adalah skenario normal yang harus ditangani dengan response error, bukan exception yang tidak tertangani.
Error Handling dan Custom Message
Setiap method validasi bisa diberi custom error message sebagai parameter kedua:
z.string().min(3, { message: "Minimal 3 karakter" });
Custom message ini akan muncul di result.error ketika validasi gagal, sehingga bisa langsung ditampilkan ke pengguna tanpa perlu mapping error code secara manual. Selain custom message per-field, Zod juga mendukung global error map untuk menyesuaikan format pesan error secara konsisten di seluruh aplikasi.
Async Validation
Beberapa validasi memerlukan operasi asynchronous, misalnya mengecek apakah sebuah username sudah terdaftar di database. Untuk kasus ini, refine() bisa menerima fungsi async:
z.string().refine(async (val) => {
const exists = await checkUser(val);
return !exists;
}, {
message: "User sudah ada",
});
Schema yang mengandung refine async tidak bisa divalidasi denganparse()atausafeParse()biasa. GunakanparseAsync()atausafeParseAsync(), atau validasi akan gagal secara tidak terduga.
Branding dan Nominal Typing
Secara default, TypeScript menggunakan structural typing — dua tipe dengan struktur sama dianggap setara meskipun secara semantik berbeda (misalnya UserId dan ProductId yang keduanya berupa string). Zod menyediakan brand() untuk membuat nominal typing, di mana tipe-tipe tersebut tidak bisa saling tertukar secara tidak sengaja:
const UserId = z.string().brand("UserId");
Fitur ini berguna pada sistem besar dengan banyak ID berbeda yang secara struktural sama-sama berupa string, tapi secara domain tidak boleh saling tertukar.
Kapan Cukup Validasi Bawaan, Kapan Perlu Refine atau Schema Terpisah?
Tidak semua kasus validasi membutuhkan fitur lanjutan. Diagram berikut membantu menentukan pendekatan yang sesuai:
flowchart TD
A{Validasi melibatkan satu field saja?} -- Ya --> B[Gunakan method bawaan: min, max, regex, dll]
A -- Tidak --> C{Melibatkan beberapa field sekaligus?}
C -- Ya --> D[Gunakan superRefine]
C -- Tidak --> E{Struktur berubah berdasarkan satu field?}
E -- Ya --> F[Gunakan discriminatedUnion]
E -- Tidak --> G[Gunakan refine atau transform sesuai kebutuhan]Best Practice Penggunaan Zod
LAKUKAN:
✓ pisahkan schema domain dan schema form
✓ gunakan discriminatedUnion untuk struktur yang kompleks
✓ manfaatkan z.infer agar type selalu sinkron dengan schema
✓ gunakan safeParse di boundary layer (API, service)
HINDARI:
✗ logic validasi di UI yang terpisah dari schema
✗ parse() langsung di boundary layer tanpa penanganan error
✗ duplikasi schema untuk struktur yang sebenarnya sama
Memisahkan schema domain (representasi data yang sebenarnya) dari schema form (representasi input mentah dari pengguna) membantu menjaga validasi tetap rapi, terutama ketika form menggunakan preprocess untuk mengubah string menjadi number atau boolean sebelum divalidasi sebagai schema domain.
Ringkasan
- Zod mendefinisikan schema, melakukan validasi runtime, dan menghasilkan type TypeScript otomatis melalui
z.infer.- Primitive type (
string,number,boolean,date) punya chained method untuk memperketat aturan, seperti.min(),.max(),.email().optional(),nullable(), dandefault()mengatur field yang tidak wajib atau punya nilai bawaan.discriminatedUnionlebih tepat dibandingunionbiasa ketika struktur object berubah berdasarkan satu field.refine()untuk validasi custom satu field,superRefine()untuk validasi yang melibatkan beberapa field.transform()mengubah data setelah validasi,preprocess()mengubah input sebelum validasi — cocok untuk form HTML.partial(),pick(),omit(), danextend()membantu menurunkan schema baru dari schema yang sudah ada tanpa duplikasi.- Gunakan
safeParse()(bukanparse()) di boundary layer seperti API atau service agar error validasi bisa ditangani secara eksplisit.- Validasi async wajib menggunakan
parseAsync()atausafeParseAsync().