Zod Dari Dasar Hingga Mahir: Panduan Lengkap Semua Jenis Validasi
9 min read

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 dengan parse() atau safeParse() biasa. Gunakan parseAsync() atau safeParseAsync(), 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(), dan default() mengatur field yang tidak wajib atau punya nilai bawaan.
  • discriminatedUnion lebih tepat dibanding union biasa 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(), dan extend() membantu menurunkan schema baru dari schema yang sudah ada tanpa duplikasi.
  • Gunakan safeParse() (bukan parse()) di boundary layer seperti API atau service agar error validasi bisa ditangani secara eksplisit.
  • Validasi async wajib menggunakan parseAsync() atau safeParseAsync().

Portofolio