Skip to content
AB

Zod Beyond Basic Parsing: Strict Objects, Defaults, Coercion, and Inferred Types

Practical Zod patterns for safer query strings, configuration, discriminated inputs, and TypeScript contracts without duplicate types.

Ashish BhakhandPublished

Basic Zod examples validate a string or an object. Real APIs also receive query parameters, environment variables, multiple command shapes, and payloads that evolve over time. A few patterns make those boundaries much clearer.

Reject unknown fields intentionally#

Code
const updateProfileSchema = z
  .object({
    displayName: z.string().trim().min(2).max(60).optional(),
    timezone: z.string().min(1).optional(),
  })
  .strict();

Strict objects catch display_name when the API expects displayName. They also prevent a caller from passing fields such as role into a general update endpoint.

Coerce only at text boundaries#

Query strings and environment variables arrive as strings:

Code
const listQuerySchema = z.object({
  page: z.coerce.number().int().min(1).default(1),
  limit: z.coerce.number().int().min(1).max(100).default(20),
  includeArchived: z
    .enum(["true", "false"])
    .transform((value) => value === "true")
    .default(false),
});

Broad boolean coercion is surprising because JavaScript treats many non-empty strings as truthy. An explicit enum makes accepted values obvious.

Model alternatives with a discriminator#

When a command has several valid shapes, a discriminated union gives precise validation and narrowing:

Code
const destinationSchema = z.discriminatedUnion("kind", [
  z.object({ kind: z.literal("email"), address: z.string().email() }),
  z.object({ kind: z.literal("push"), deviceToken: z.string().min(1) }),
]);

type Destination = z.infer<typeof destinationSchema>;

Code switching on kind now receives the correct fields without casts.

Validate configuration at startup#

Code
const environmentSchema = z.object({
  MONGODB_URI: z.string().url(),
  JWT_AUDIENCE: z.string().min(1),
  LOG_LEVEL: z.enum(["debug", "info", "warn", "error"]).default("info"),
});

export const environment = environmentSchema.parse(process.env);

Failing during startup is better than discovering a missing variable halfway through a request.

Use transforms sparingly. A schema that performs hidden business work becomes hard to understand. Let schemas normalize boundary representation; let use cases make business decisions. With inferred types, the validator and TypeScript contract cannot quietly drift apart.

← Back to all writing