Skip to main content

When to Use

When validating objects that share a base shape but have type-specific required/optional fields — distinguished by a discriminator field (like type, kind, or variant).

Pattern

import { z } from "zod";

// 1. Define base schema with shared fields
const BaseSchema = z.object({
  id: z.string(),
  type: z.enum(["alpha", "beta", "gamma"]),
  name: z.string(),
});

// 2. Each variant extends base with z.literal() override
const AlphaSchema = BaseSchema.extend({
  type: z.literal("alpha"),
  alpha_field: z.string(),  // required only for alpha
});

const BetaSchema = BaseSchema.extend({
  type: z.literal("beta"),
  beta_field: z.number(),
  optional_field: z.string().optional(),
});

// 3. Create discriminated union
const ThingSchema = z.discriminatedUnion("type", [
  AlphaSchema,
  BetaSchema,
]);

// 4. Infer TypeScript types from Zod
type Thing = z.infer<typeof ThingSchema>;

Why Not z.union()?

z.discriminatedUnion("type", [...]) is faster — it reads the discriminator field first and only validates the matching branch. z.union([...]) tries every branch sequentially and produces confusing error messages.

Gotcha: .extend() Strips Unknown Keys

Zod schemas with .extend() use .strip() mode by default — unknown keys are silently removed from the output. If you need to detect forbidden keys (like “composition must not appear on fragments”), check the raw input:
const raw = parsed as Record<string, unknown>;
if (unit.type === "fragment" && "composition" in raw) {
  errors.push("composition is forbidden for fragments");
}

Gotcha: .default() Injects Values

const schema = z.object({
  gate: z.boolean().default(false),
});

schema.parse({});              // → { gate: false }
schema.parse({ gate: true });  // → { gate: true }
The output type includes gate: boolean (not gate?: boolean).

JSON Schema Generation

import { zodToJsonSchema } from "zod-to-json-schema";

const jsonSchema = zodToJsonSchema(AlphaSchema, "Alpha");
// → standard JSON Schema object for non-TS consumers
Keeps Zod as the single source of truth — JSON Schema is derived, never hand-maintained.