Zod Conventions
Zod Schema Conventions (Zod v4)
Section titled “Zod Schema Conventions (Zod v4)”Project uses Zod v4 (zod catalog ^4.4.3). v4 moved every string-format
validator OFF z.string() and onto top-level functions. The old method forms
(z.string().email(), .uuid(), .url(), …) still run but are @deprecated —
struck-through in the editor, slated for removal in a future major.
Rule: always use the top-level / z.iso form. Never the z.string().<format>()
method chain.
Format validators — top-level, not methods
Section titled “Format validators — top-level, not methods”| Deprecated (do NOT write) | Use (v4) |
|---|---|
z.string().uuid() | z.uuid() |
z.string().email() | z.email() |
z.string().url() | z.url() |
z.string().emoji() | z.emoji() |
z.string().nanoid() / .cuid() / .cuid2() / .ulid() | z.nanoid() / z.cuid() / z.cuid2() / z.ulid() |
z.string().ipv4() / .ipv6() | z.ipv4() / z.ipv6() |
z.string().base64() / .base64url() | z.base64() / z.base64url() |
z.string().jwt() | z.jwt() |
ISO date/time — z.iso namespace
Section titled “ISO date/time — z.iso namespace”ISO string formats moved under z.iso:
| Deprecated | Use |
|---|---|
z.string().date() | z.iso.date() |
z.string().time() | z.iso.time() |
z.string().datetime() | z.iso.datetime() |
z.string().duration() | z.iso.duration() |
CRITICAL — z.date() ≠ z.string().date()
Section titled “CRITICAL — z.date() ≠ z.string().date()”Do NOT “fix” z.date(). Same word, opposite meaning:
z.date()— validates a JSDateinstance (ZodDate). Used bycreated_at/updated_atoutput schemas. KEEP — not deprecated.z.coerce.date()— coerces input to aDate. KEEP.z.string().date()— validates an ISO date STRING (YYYY-MM-DD). This is the deprecated one →z.iso.date().
Converting z.date() → z.iso.date() is a bug (changes Date-object validation
into string validation). When migrating, only touch z.string().<format>() chains.
Error messages
Section titled “Error messages”Top-level validators take the message as a bare string arg:
z.uuid("ID tidak valid"), z.iso.date("Format tanggal: YYYY-MM-DD").
Required + format (two distinct messages) — pipe a non-empty z.string() into the
format validator so the “required” message fires before the “format” message:
// empty → "Email wajib diisi"; non-empty invalid → "Format email tidak valid"email: z.string().min(1, "Email wajib diisi").pipe(z.email("Format email tidak valid")),Still valid on z.string()
Section titled “Still valid on z.string()”Length/content constraints stay as z.string() methods: .min(), .max(),
.length(), .regex(), .trim(), .startsWith(), .endsWith(), .includes(),
.toLowerCase(), .toUpperCase(). Only the format validators moved off.