zod-patterns

Zod schema validation, type-safe development, and strict TypeScript patterns. When user works with Zod, validates data, creates schemas, handles form validation, mentions z.object/z.string patterns, needs runtime validation, type-safe code, or strict TypeScript configuration.

15 stars

Best use case

zod-patterns is best used when you need a repeatable AI agent workflow instead of a one-off prompt.

Zod schema validation, type-safe development, and strict TypeScript patterns. When user works with Zod, validates data, creates schemas, handles form validation, mentions z.object/z.string patterns, needs runtime validation, type-safe code, or strict TypeScript configuration.

Teams using zod-patterns should expect a more consistent output, faster repeated execution, less prompt rewriting.

When to use this skill

  • You want a reusable workflow that can be run more than once with consistent structure.

When not to use this skill

  • You only need a quick one-off answer and do not need a reusable workflow.
  • You cannot install or maintain the underlying files, dependencies, or repository context.

Installation

Claude Code / Cursor / Codex

$curl -o ~/.claude/skills/zod-patterns/SKILL.md --create-dirs "https://raw.githubusercontent.com/shepherdjerred/monorepo/main/packages/dotfiles/dot_agents/skills/zod-patterns/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/zod-patterns/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How zod-patterns Compares

Feature / Agentzod-patternsStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/A

Frequently Asked Questions

What does this skill do?

Zod schema validation, type-safe development, and strict TypeScript patterns. When user works with Zod, validates data, creates schemas, handles form validation, mentions z.object/z.string patterns, needs runtime validation, type-safe code, or strict TypeScript configuration.

Where can I find the source code?

You can find the source code on GitHub using the link provided at the top of the page.

SKILL.md Source

# Zod Patterns Agent

## What's New in Zod 4 (2025)

- **Performance**: `z.array()` 7.4x faster, `z.object()` 6.5x faster (compared to Zod v3)
- **Zod Mini**: Tree-shakable functional API variant
- **Metadata API**: Attach custom metadata to schemas
- **JSON Schema**: Built-in JSON Schema generation
- **Locales**: Built-in i18n support for error messages
- **`z.looseObject()`**: Replaces `.passthrough()`
- **`z.strictObject()`**: Replaces `.strict()`

## Installation

```bash
# Standard Zod v4 (recommended)
bun add zod

# Minimal bundle for tree-shaking (1.9KB gzipped)
bun add @zod/mini
```

Requires TypeScript 5.5+ with `"strict": true` in tsconfig.

### Using @zod/mini

```typescript
// Tree-shakable imports (only includes what you use)
import { z } from "@zod/mini";

// Same API as full Zod
const UserSchema = z.object({
  id: z.string(),
  email: z.string().email(),
});

// Results in significantly smaller bundles
// Full Zod: ~5KB | @zod/mini: ~1.9KB (tree-shaken)
```

For library authors targeting v4: `import { z } from "zod/v4/core";`

## Basic Usage

### Defining and Parsing

```typescript
import { z } from "zod";

// Define a schema
const UserSchema = z.object({
  name: z.string(),
  email: z.string().email(),
  age: z.number().int().positive(),
});

// Parse data (throws on invalid)
const user = UserSchema.parse({
  name: "Alice",
  email: "alice@example.com",
  age: 25,
});

// Safe parse (returns result object)
const result = UserSchema.safeParse(data);
if (result.success) {
  console.log(result.data); // typed as User
} else {
  console.log(result.error.issues); // validation errors
}
```

### Type Inference

```typescript
// Infer TypeScript type from schema
type User = z.infer<typeof UserSchema>;
// { name: string; email: string; age: number }

// For schemas with transforms - input vs output types
type UserInput = z.input<typeof UserSchema>;
type UserOutput = z.output<typeof UserSchema>;
```

## Primitive Types

### Strings

```typescript
z.string(); // any string
z.string().min(1); // non-empty
z.string().max(100); // max length
z.string().length(5); // exact length
z.string().email(); // email format
z.string().url(); // URL format
z.string().uuid(); // UUID format
z.string().cuid(); // CUID format
z.string().cuid2(); // CUID2 format
z.string().ulid(); // ULID format
z.string().regex(/^[a-z]+$/); // custom regex
z.string().includes("@"); // contains substring
z.string().startsWith("http"); // starts with
z.string().endsWith(".com"); // ends with
z.string().datetime(); // ISO datetime
z.string().date(); // ISO date
z.string().time(); // ISO time
z.string().ip(); // IP address
z.string().trim(); // trim whitespace (transform)
z.string().toLowerCase(); // lowercase (transform)
z.string().toUpperCase(); // uppercase (transform)
```

### Numbers

```typescript
z.number(); // any number
z.number().int(); // integer only
z.number().positive(); // > 0
z.number().nonnegative(); // >= 0
z.number().negative(); // < 0
z.number().nonpositive(); // <= 0
z.number().min(5); // >= 5
z.number().max(100); // <= 100
z.number().gt(5); // > 5
z.number().gte(5); // >= 5 (alias for min)
z.number().lt(100); // < 100
z.number().lte(100); // <= 100 (alias for max)
z.number().multipleOf(5); // divisible by 5
z.number().finite(); // not Infinity
z.number().safe(); // within safe integer range
```

### Other Primitives

```typescript
z.boolean(); // true or false
z.bigint(); // BigInt values
z.date(); // Date objects
z.undefined(); // undefined only
z.null(); // null only
z.void(); // undefined (for function returns)
z.any(); // bypass validation
z.unknown(); // any, but type-safe usage
z.never(); // always fails
```

## Coercion

Automatically convert input types before validation:

```typescript
// String coercion
z.coerce.string(); // String(input)
z.coerce.number(); // Number(input)
z.coerce.boolean(); // Boolean(input)
z.coerce.bigint(); // BigInt(input)
z.coerce.date(); // new Date(input)

// With validation
const ageSchema = z.coerce.number().int().positive();
ageSchema.parse("25"); // 25
ageSchema.parse("abc"); // throws - NaN is not positive

// Common pitfall: empty string becomes 0
z.coerce.number().parse(""); // 0 (might not be desired)

// Fix: preprocess to handle empty strings
const safeNumber = z.preprocess(
  (val) => (val === "" ? undefined : val),
  z.coerce.number(),
);
```

## Objects

### Basic Objects

```typescript
const PersonSchema = z.object({
  name: z.string(),
  age: z.number(),
  email: z.string().email().optional(),
});

// By default, unknown keys are stripped
PersonSchema.parse({ name: "Bob", age: 30, extra: "ignored" });
// { name: "Bob", age: 30 }
```

### Object Modes (Zod 4)

```typescript
// Standard - strips unknown keys
z.object({ name: z.string() });

// Loose - passes through unknown keys
z.looseObject({ name: z.string() });

// Strict - rejects unknown keys
z.strictObject({ name: z.string() });
```

### Object Manipulation

```typescript
const UserSchema = z.object({
  id: z.string().uuid(),
  name: z.string(),
  email: z.string().email(),
  password: z.string(),
  createdAt: z.date(),
});

// Pick specific fields
const PublicUser = UserSchema.pick({ id: true, name: true, email: true });

// Omit sensitive fields
const SafeUser = UserSchema.omit({ password: true });

// Make all fields optional
const PartialUser = UserSchema.partial();

// Make specific fields optional
const UpdateUser = UserSchema.partial({ name: true, email: true });

// Make all fields required
const RequiredUser = UserSchema.required();

// Extend with additional fields
const AdminSchema = UserSchema.extend({
  role: z.literal("admin"),
  permissions: z.array(z.string()),
});

// Merge schemas (Zod 4 - use extend instead)
const Combined = BaseSchema.extend(ExtraSchema.shape);
```

### Nested Objects

```typescript
const AddressSchema = z.object({
  street: z.string(),
  city: z.string(),
  country: z.string(),
  zip: z.string().optional(),
});

const CompanySchema = z.object({
  name: z.string(),
  address: AddressSchema,
  employees: z.array(
    z.object({
      name: z.string(),
      department: z.string(),
    }),
  ),
});
```

## Arrays and Collections

### Arrays

```typescript
z.array(z.string()); // string[]
z.array(z.string()).min(1); // at least 1 element
z.array(z.string()).max(10); // at most 10 elements
z.array(z.string()).length(5); // exactly 5 elements
z.array(z.string()).nonempty(); // same as .min(1)

// Access element schema
const arr = z.array(z.string());
arr.element; // z.string()
```

### Tuples

```typescript
// Fixed-length array with specific types
const PointSchema = z.tuple([z.number(), z.number()]);
type Point = z.infer<typeof PointSchema>; // [number, number]

// With rest elements
const ArgsSchema = z.tuple([z.string(), z.number()]).rest(z.boolean());
// [string, number, ...boolean[]]
```

### Records and Maps

```typescript
// Record<string, T>
z.record(z.string(), z.number()); // { [key: string]: number }
z.record(z.number()); // shorthand for string keys

// Map<K, V>
z.map(z.string(), z.number()); // Map<string, number>

// Set<T>
z.set(z.string()); // Set<string>
z.set(z.number()).min(1).max(10); // with size constraints
```

## Unions and Enums

### Unions

```typescript
// Basic union
const StringOrNumber = z.union([z.string(), z.number()]);
type SN = z.infer<typeof StringOrNumber>; // string | number

// Shorthand
const StringOrNumber2 = z.string().or(z.number());
```

### Discriminated Unions

```typescript
// More efficient parsing with discriminator key
const ResultSchema = z.discriminatedUnion("status", [
  z.object({
    status: z.literal("success"),
    data: z.object({ id: z.string() }),
  }),
  z.object({
    status: z.literal("error"),
    message: z.string(),
  }),
]);

type Result = z.infer<typeof ResultSchema>;
// { status: "success"; data: { id: string } } | { status: "error"; message: string }

// TypeScript narrows based on discriminator
const result = ResultSchema.parse(data);
if (result.status === "success") {
  console.log(result.data.id); // typed correctly
}
```

### Enums

```typescript
// Zod enum (recommended)
const StatusSchema = z.enum(["pending", "active", "inactive"]);
type Status = z.infer<typeof StatusSchema>; // "pending" | "active" | "inactive"

// Access values
StatusSchema.options; // ["pending", "active", "inactive"]
StatusSchema.enum; // { pending: "pending", active: "active", ... }

// Extract or exclude values
StatusSchema.extract(["pending", "active"]); // only these
StatusSchema.exclude(["inactive"]); // all except these

// Native enum
enum NativeStatus {
  Pending,
  Active,
  Inactive,
}
const NativeStatusSchema = z.nativeEnum(NativeStatus);
```

### Literals

```typescript
z.literal("hello"); // exactly "hello"
z.literal(42); // exactly 42
z.literal(true); // exactly true
z.null(); // exactly null
z.undefined(); // exactly undefined
```

## Optional, Nullable, Default

```typescript
// Optional - allows undefined
z.string().optional(); // string | undefined

// Nullable - allows null
z.string().nullable(); // string | null

// Nullish - allows null or undefined
z.string().nullish(); // string | null | undefined

// Default values
z.string().default("N/A"); // static default
z.number().default(() => Math.random()); // dynamic default
z.date().default(() => new Date()); // current date

// Catch - use default on any parse error
z.number().catch(0); // returns 0 on error
z.string().catch((ctx) => `Error: ${ctx.error.message}`);

// Unwrap optional/nullable
const optStr = z.string().optional();
optStr.unwrap(); // z.string()
```

## Refinements and Transforms

### Refinements (Custom Validation)

```typescript
// Basic refine
const PasswordSchema = z
  .string()
  .min(8, "Password must be at least 8 characters")
  .refine((val) => /[A-Z]/.test(val), "Password must contain uppercase letter")
  .refine((val) => /[0-9]/.test(val), "Password must contain number");

// With custom error path
const FormSchema = z
  .object({
    password: z.string(),
    confirmPassword: z.string(),
  })
  .refine((data) => data.password === data.confirmPassword, {
    message: "Passwords don't match",
    path: ["confirmPassword"], // error appears on this field
  });
```

### SuperRefine (Multiple Issues)

```typescript
const ComplexSchema = z.string().superRefine((val, ctx) => {
  if (val.length < 8) {
    ctx.addIssue({
      code: z.ZodIssueCode.too_small,
      minimum: 8,
      type: "string",
      inclusive: true,
      message: "Too short",
    });
  }
  if (!/[A-Z]/.test(val)) {
    ctx.addIssue({
      code: z.ZodIssueCode.custom,
      message: "Missing uppercase",
    });
  }
});
```

### Transforms

```typescript
// Transform output type
const TrimmedString = z.string().transform((val) => val.trim());

const NumberFromString = z
  .string()
  .transform((val) => parseInt(val, 10))
  .pipe(z.number().int());

const DateFromISO = z
  .string()
  .datetime()
  .transform((val) => new Date(val));

// Transform with validation
const PositiveFromString = z
  .string()
  .transform((val, ctx) => {
    const num = parseInt(val, 10);
    if (isNaN(num)) {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: "Not a number",
      });
      return z.NEVER;
    }
    return num;
  })
  .pipe(z.number().positive());
```

### Preprocess

```typescript
// Transform BEFORE parsing
const TrimmedEmail = z.preprocess(
  (val) => (typeof val === "string" ? val.trim().toLowerCase() : val),
  z.string().email(),
);

// Handle empty strings
const OptionalNumber = z.preprocess(
  (val) => (val === "" ? undefined : val),
  z.coerce.number().optional(),
);
```

### Pipe (Chaining Schemas)

```typescript
// Chain multiple schemas
const ParsedInt = z
  .string()
  .transform((val) => parseInt(val, 10))
  .pipe(z.number().int().positive());

// With intermediate validation
const EmailDomain = z
  .string()
  .email()
  .pipe(z.string().endsWith("@company.com"));
```

## Error Handling

### Custom Error Messages

```typescript
// Per-validator messages
z.string().min(5, "Must be at least 5 characters");
z.string().min(5, { message: "Too short" });

// Schema-level errors
z.string("Expected a string");
z.string({
  error: (issue) =>
    issue.input === undefined ? "Field is required" : "Must be a string",
});
```

### Parsing Errors

```typescript
const result = UserSchema.safeParse(input);

if (!result.success) {
  // Formatted errors
  const formatted = result.error.format();
  // { name: { _errors: ["Required"] }, email: { _errors: ["Invalid email"] } }

  // Flattened errors
  const flat = result.error.flatten();
  // { formErrors: [], fieldErrors: { name: ["Required"], email: ["Invalid email"] } }

  // Raw issues
  result.error.issues.forEach((issue) => {
    console.log(issue.path, issue.message, issue.code);
  });
}
```

### Error Maps

```typescript
// Per-parse error customization
schema.parse(data, {
  error: (issue) => `Custom error: ${issue.code}`,
});

// Global configuration
z.config({
  customError: (issue) => {
    if (issue.code === "invalid_type") {
      return `Expected ${issue.expected}, got ${issue.received}`;
    }
    return issue.message;
  },
});

// Localization
import { en } from "zod/v4/locales/en";
z.config(en());
```

## Advanced Patterns

### Branded Types

```typescript
const UserId = z.string().uuid().brand<"UserId">();
const PostId = z.string().uuid().brand<"PostId">();

type UserId = z.infer<typeof UserId>;
type PostId = z.infer<typeof PostId>;

function getUser(id: UserId) {
  /* ... */
}

const userId = UserId.parse("550e8400-e29b-41d4-a716-446655440000");
const postId = PostId.parse("550e8400-e29b-41d4-a716-446655440000");

getUser(userId); // OK
getUser(postId); // Type error!
```

### Recursive Types

```typescript
interface Category {
  name: string;
  subcategories: Category[];
}

const CategorySchema: z.ZodType<Category> = z.lazy(() =>
  z.object({
    name: z.string(),
    subcategories: z.array(CategorySchema),
  }),
);
```

### JSON Type

```typescript
// Generic JSON value
const JsonValue: z.ZodType<unknown> = z.lazy(() =>
  z.union([
    z.string(),
    z.number(),
    z.boolean(),
    z.null(),
    z.array(JsonValue),
    z.record(JsonValue),
  ]),
);
```

### Function Schemas

```typescript
const FnSchema = z.function().args(z.string(), z.number()).returns(z.boolean());

type Fn = z.infer<typeof FnSchema>;
// (args_0: string, args_1: number) => boolean

const validated = FnSchema.implement((name, age) => {
  return age > 18;
});
```

### Async Validation

```typescript
const UniqueEmailSchema = z
  .string()
  .email()
  .refine(async (email) => {
    const exists = await checkEmailExists(email);
    return !exists;
  }, "Email already taken");

// Must use async parse
const result = await UniqueEmailSchema.safeParseAsync("test@example.com");
```

## Integration Patterns

### Form Validation

```typescript
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";

const FormSchema = z.object({
  email: z.string().email("Invalid email"),
  password: z.string().min(8, "At least 8 characters"),
});

function MyForm() {
  const { register, handleSubmit, formState: { errors } } = useForm({
    resolver: zodResolver(FormSchema),
  });

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register("email")} />
      {errors.email && <span>{errors.email.message}</span>}
      {/* ... */}
    </form>
  );
}
```

### API Validation

```typescript
// Hono example
import { zValidator } from "@hono/zod-validator";

const CreateUserSchema = z.object({
  name: z.string().min(1),
  email: z.string().email(),
});

app.post("/users", zValidator("json", CreateUserSchema), async (c) => {
  const data = c.req.valid("json");
  // data is fully typed
});
```

### Environment Variables

```typescript
const EnvSchema = z.object({
  DATABASE_URL: z.string().url(),
  PORT: z.coerce.number().default(3000),
  NODE_ENV: z
    .enum(["development", "production", "test"])
    .default("development"),
  API_KEY: z.string().min(1),
});

export const env = EnvSchema.parse(process.env);
```

## Replacing Common Type Checks with Zod

Prefer Zod validation over `typeof`, `instanceof`, or type guard functions.

### typeof / instanceof / Number checks → Zod

```typescript
// ❌ Avoid typeof operator
if (typeof value === "string") {
  return value.toUpperCase();
}

// ✅ Prefer Zod validation
const result = z.string().safeParse(value);
if (result.success) {
  return result.data.toUpperCase();
}

// ❌ Avoid instanceof
if (err instanceof Error) {
  console.log(err.message);
}

// ✅ Prefer Zod
const result = z.instanceof(Error).safeParse(err);
if (result.success) {
  console.log(result.data.message);
}

// ❌ Avoid Number.isInteger
if (Number.isInteger(value)) {
  return value * 2;
}

// ✅ Prefer Zod
const result = z.number().int().safeParse(value);
if (result.success) {
  return result.data * 2;
}
```

### Type predicates → Zod

```typescript
// ❌ Avoid type guard functions
function isUser(value: unknown): value is User {
  return typeof value === "object" && value !== null && "email" in value;
}

// ✅ Prefer Zod schema validation
const UserSchema = z.object({
  email: z.string().email(),
  name: z.string(),
});

const result = UserSchema.safeParse(value);
if (result.success) {
  const user = result.data; // Type-safe!
}
```

## Type Assertions Rules

Only `as unknown` and `as const` are allowed — all other `as` casts are banned.

```typescript
// ❌ Never do this - bypasses type safety
const user = data as User;
const id = value as string;

// ✅ Cast to unknown first, then validate
const data = response as unknown;
const result = UserSchema.safeParse(data);
if (result.success) {
  const user = result.data;
}

// ✅ Use 'as const' for literal types
const STATUSES = ["pending", "approved", "rejected"] as const;
type Status = (typeof STATUSES)[number];
```

**Why no type assertions?** They bypass TypeScript's type checking, don't perform runtime validation, can cause runtime crashes, and hide bugs instead of catching them. Zod gives you both compile-time AND runtime safety.

## Strict TypeScript Configuration

### tsconfig.json Best Practices

```json
{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "noPropertyAccessFromIndexSignature": true,
    "exactOptionalPropertyTypes": true,
    "noImplicitOverride": true,
    "noFallthroughCasesInSwitch": true,
    "forceConsistentCasingInFileNames": true
  }
}
```

### Use 'type' Instead of 'interface'

```typescript
// ✅ Prefer type
type User = {
  id: string;
  email: string;
};

type Admin = User & {
  permissions: string[];
};

// ❌ Avoid interface
interface User {
  id: string;
  email: string;
}
```

**Why?** Types are more flexible (unions, intersections) and consistent with Zod's inferred types.

## Best Practices Summary

1. **Use `safeParse()`** - avoid throwing in production code
2. **Infer types** - let Zod generate TypeScript types
3. **Compose schemas** - use `extend`, `pick`, `omit` for reuse
4. **Prefer coercion** over preprocess for type conversion
5. **Add custom messages** - improve user-facing errors
6. **Use discriminated unions** - for efficient tagged unions
7. **Brand sensitive types** - prevent accidental ID swapping
8. **Validate at boundaries** - API inputs, env vars, external data
9. **Use transforms sparingly** - keep schemas focused on validation
10. **Test edge cases** - empty strings, null, undefined

## When to Ask for Help

- Complex recursive schema patterns
- Performance optimization for high-throughput validation
- Integration with specific ORMs or frameworks
- Migration from other validation libraries
- Custom error formatting requirements
- Schema generation from external sources

Related Skills

argocd-app-patterns

15
from shepherdjerred/monorepo

Use when asking about ArgoCD applications, Helm chart deployment via ArgoCD, sync policies, or typed Helm values patterns.

toolkit-recall

15
from shepherdjerred/monorepo

Search past decisions, prior research, conversation history, monorepo docs, and fetched web pages. Use toolkit recall search to find context from previous work, and toolkit fetch to save web pages for future search.

zellij-helper

15
from shepherdjerred/monorepo

Zellij terminal multiplexer for session management, layouts, and pane operations When user mentions Zellij, terminal multiplexer, zellij commands, sessions, panes, layouts, or tabs

worktree-workflow

15
from shepherdjerred/monorepo

Git worktree workflow for isolated feature development and PR creation When user starts new work, needs to switch contexts, or wants parallel development

vite-react-helper

15
from shepherdjerred/monorepo

Vite + React for fast modern web development - build config, HMR, hooks, state management, and performance patterns When user works with Vite, React, creates components, manages state, uses hooks, or configures Vite builds

version-management

15
from shepherdjerred/monorepo

Use when asking about version management, Renovate annotations, versions.ts patterns, or pinning image/chart versions.

typst-authoring

15
from shepherdjerred/monorepo

This skill should be used when the user asks to "write Typst", "create a Typst document", "format in Typst", "convert to Typst", "Typst syntax", "Typst template", "Typst math", "Typst table", or works with .typ files. Provides comprehensive Typst markup, scripting, math, layout, and styling reference for authoring documents. Also use proactively when generating .typ output files (e.g., in deep-research reports).

typescript-helper

15
from shepherdjerred/monorepo

TypeScript development guidance for type systems and tooling When user works with .ts or .tsx files, mentions TypeScript, or encounters type errors

torvalds-deployment

15
from shepherdjerred/monorepo

Use when asking about adding services, createXxxDeployment patterns, or homelab deployment conventions. Services use per-namespace charts.

terraform-helper

15
from shepherdjerred/monorepo

Terraform and OpenTofu infrastructure as code - HCL, providers, modules, state management, and CLI operations When user works with .tf files, mentions Terraform, OpenTofu, tofu, HCL, infrastructure as code, or tf commands

terminal-concepts

15
from shepherdjerred/monorepo

Comprehensive guide for building CLI and TUI applications - terminal internals, design principles, and battle-tested patterns When building CLI/TUI apps, implementing argument parsing, handling terminal input/output, escape codes, buffering, signals, or asking about terminal development concepts

talos-helper

15
from shepherdjerred/monorepo

Talos Linux cluster administration using talosctl When user mentions Talos, talosctl, or Talos cluster operations