Skip to content

Zod Conventions

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 string formats moved under z.iso:

DeprecatedUse
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 JS Date instance (ZodDate). Used by created_at/updated_at output schemas. KEEP — not deprecated.
  • z.coerce.date() — coerces input to a Date. 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.

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")),

Length/content constraints stay as z.string() methods: .min(), .max(), .length(), .regex(), .trim(), .startsWith(), .endsWith(), .includes(), .toLowerCase(), .toUpperCase(). Only the format validators moved off.