agentleFS
AlteriomAlteriom/ai-dev-skills/skills/zod/SKILL.md

zod

Skill0 starsChanged 3 months agoMITView on GitHub

TypeScript-first schema validation with static type inference. Build type-safe applications with runtime validation, form handling, and API contracts.

  • Reads credentials
  • Installs packages

23 other projects use a version of this.

Adopted by

Most recently taken up by

Used with

Projects with it also use tailwind-css-patterns, typescript-advanced-types and 3 more.

Other files people use alongside this one.

Files found in the same public projects as a version of this one, more often than they turn up anywhere else. Each project counts once, large collections count for less, and this file's own repository is left out.
  • midudevSkill

    tailwind-css-patterns

    Provides comprehensive Tailwind CSS utility-first styling patterns including responsive design, layout utilities, flexbox, grid, spacing, typography, colors, and modern CSS best practices. Use when styling React/Vue/Svelte components, building responsive layouts, implementing design systems, or optimizing CSS workflow.

    Also in 3 projects that use this

  • xfstudioSkill

    typescript-advanced-types

    Master TypeScript's advanced type system including generics, conditional types, mapped types, template literals, and utility types for building type-safe applications. Use when implementing complex type logic, creating reusable type utilities, or ensuring compile-time type safety in TypeScript projects.

    Also in 5 projects that use this

  • VercelSkill

    turborepo

    Turborepo monorepo build system guidance. Triggers on: turbo.json, task pipelines, dependsOn, caching, remote cache, the "turbo" CLI, --filter, --affected, CI optimization, environment variables, internal packages, monorepo structure/best practices, and boundaries.

    Also in 4 projects that use this

  • VercelSkill

    ai-sdk

    Answer questions about the AI SDK and help build AI-powered features. Use when developers: (1) Ask about AI SDK functions like generateText, streamText, ToolLoopAgent, embed, or tools, (2) Want to build AI agents, chatbots, RAG systems, or text generation features, (3) Have questions about AI providers (OpenAI, Anthropic, Google, etc.), streaming, tool calling, structured output, or embeddings, (4) Use React hooks like useChat or useCompletion. Triggers on: "AI SDK", "Vercel AI SDK", "generateText", "streamText", "add AI to my app", "build an agent", "tool calling", "structured output", "useChat".

    Also in 3 projects that use this

  • BlockSkill

    frontend-design

    Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.

    Also in 4 projects that use this

The file

We split this skill into the rules it gives an agent

9 rules. 0 are common, 9 are distinctive, and 0 are very specific to this project.

  • 0

    Common

    Most of the 23 edited versions we read keep these.

  • 9

    Distinctive

    This author's own choices. Worth a look.

  • 0

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Use this skill when:

    line 25
  2. Distinctive:

    Don't use: when you only need TypeScript types (no runtime validation), or when validation logic is too complex for declarative schemas …

    line 36
  3. Distinctive:

    Karpathy Principle: Simplicity First: - Start with primitive validators.

    line 130
  4. Distinctive:

    Karpathy Principle: Goal-Driven Execution: - Use discriminated unions for type-safe state machines.

    line 286
  5. Distinctive:

    Karpathy Principle: Surgical Changes: - Use transforms to normalize input data (trim, lowercase) early in your pipeline.

    line 366
  6. Distinctive:

    Before deploying Zod validation:

    line 685
  7. Distinctive:

    Use Zod for runtime validation, TypeScript for compile-time

    line 713
  8. Distinctive:

    Use with React Hook Form (@hookform/resolvers/zod)

    line 717
  9. Distinctive:

    Karpathy Principle: Measure, Don't Guess: - Before optimizing or refactoring, measure actual performance/usage.

    line 776

“Common” is measured: kept by at least 6 in 10 of the 23 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Zod - TypeScript-First Schema Validation

When to Use

Use this skill when:

  • Validating API request/response data at runtime
  • Building type-safe forms with runtime validation
  • Parsing external data (user input, environment variables, config files)
  • Creating type-safe database schemas with Prisma
  • Validating webhook payloads
  • Building tRPC endpoints with type-safe contracts
  • Migrating from other validation libraries (Yup, Joi, Ajv)
  • Need both compile-time TypeScript types AND runtime validation

Don't use when you only need TypeScript types (no runtime validation), or when validation logic is too complex for declarative schemas (use custom validators).

Karpathy Principle: Think Before Coding - Design your schemas to make invalid states unrepresentable. If your type system says it's valid, your runtime should never see invalid data.

Prerequisites

Required Knowledge

  • TypeScript fundamentals (types, interfaces, generics)
  • Understanding of runtime vs compile-time validation
  • Basic async/await patterns

Required Tools

# Install Zod
npm install zod
# or
pnpm add zod
# or
yarn add zod

# TypeScript 4.5+ required
npm install --save-dev typescript@latest

Quick Start

import { z } from "zod"

// Define schema
const UserSchema = z.object({
  id: z.string().uuid(),
  name: z.string().min(1),
  email: z.string().email(),
  age: z.number().int().positive().optional()
})

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

// Parse data (throws on invalid)
const user = UserSchema.parse(data)

// Safe parse (returns result object)
const result = UserSchema.safeParse(data)
if (result.success) {
  console.log(result.data)  // Valid user
} else {
  console.error(result.error)  // Validation errors
}

Core Workflows

1. Primitive Types

import { z } from "zod"

// Strings
const name = z.string()
const email = z.string().email()
const url = z.string().url()
const uuid = z.string().uuid()
const minLength = z.string().min(3)
const maxLength = z.string().max(100)
const pattern = z.string().regex(/^[A-Z]+$/)
const trim = z.string().trim()  // Removes whitespace

// Numbers
const age = z.number()
const positive = z.number().positive()
const negative = z.number().negative()
const int = z.number().int()
const range = z.number().min(0).max(100)
const safe = z.number().safe()  // Safe integer range

// Booleans
const isActive = z.boolean()

// Dates
const createdAt = z.date()
const minDate = z.date().min(new Date("2020-01-01"))
const maxDate = z.date().max(new Date())

// Special types
const any = z.any()  // Allows anything (avoid when possible)
const unknown = z.unknown()  // Like any but safer
const never = z.never()  // Never accepts any value
const void_ = z.void()  // Undefined only
const undefined_ = z.undefined()
const null_ = z.null()

Karpathy Principle: Simplicity First - Start with primitive validators. Only add refinements (.min, .max, .regex) when your domain actually requires them.

2. Object Schemas

// Basic object
const UserSchema = z.object({
  name: z.string(),
  email: z.string().email(),
  age: z.number().optional()
})

// Nested objects
const AddressSchema = z.object({
  street: z.string(),
  city: z.string(),
  zipCode: z.string().regex(/^\d{5}$/)
})

const PersonSchema = z.object({
  name: z.string(),
  address: AddressSchema  // Nested schema
})

// Optional and nullable properties
const UserSchema = z.object({
  name: z.string(),
  nickname: z.string().optional(),  // Can be undefined
  bio: z.string().nullable(),       // Can be null
  avatar: z.string().nullish()      // Can be null OR undefined
})

// Default values
const ConfigSchema = z.object({
  port: z.number().default(3000),
  host: z.string().default("localhost")
})

const config = ConfigSchema.parse({})
// { port: 3000, host: "localhost" }

// Strict vs passthrough
const strict = z.object({ name: z.string() }).strict()
strict.parse({ name: "Alice", extra: "field" })  // ❌ Error

const passthrough = z.object({ name: z.string() }).passthrough()
passthrough.parse({ name: "Alice", extra: "field" })  // ✅ { name: "Alice", extra: "field" }

const strip = z.object({ name: z.string() }).strip()  // Default behavior
strip.parse({ name: "Alice", extra: "field" })  // ✅ { name: "Alice" }

// Partial, required, pick, omit
type User = { name: string; email: string; age: number }
const UserSchema = z.object({ name: z.string(), email: z.string(), age: z.number() })

const PartialUser = UserSchema.partial()  // All optional
const RequiredUser = UserSchema.required()  // All required
const PickedUser = UserSchema.pick({ name: true, email: true })  // Pick fields
const OmittedUser = UserSchema.omit({ age: true })  // Omit fields

// Merge schemas
const BaseSchema = z.object({ id: z.string() })
const UserSchema = z.object({ name: z.string(), email: z.string() })
const FullUser = BaseSchema.merge(UserSchema)
// { id: string; name: string; email: string }

// Extend schemas
const TimestampedUser = UserSchema.extend({
  createdAt: z.date(),
  updatedAt: z.date()
})

3. Arrays and Tuples

// Arrays
const strings = z.array(z.string())
const numbers = z.array(z.number())
const users = z.array(UserSchema)

// Array constraints
const minLength = z.array(z.string()).min(1)  // At least 1 item
const maxLength = z.array(z.string()).max(10)  // At most 10 items
const exactLength = z.array(z.string()).length(5)  // Exactly 5 items
const nonEmpty = z.array(z.string()).nonempty()  // At least 1 item

// Tuples (fixed length, typed positions)
const point = z.tuple([z.number(), z.number()])  // [x, y]
const person = z.tuple([z.string(), z.number()])  // [name, age]

// Tuple with rest
const args = z.tuple([z.string(), z.number()]).rest(z.boolean())
// [string, number, ...boolean[]]

// Sets
const uniqueStrings = z.set(z.string())
const uniqueNumbers = z.set(z.number().int()).min(3).max(10)

// Maps
const userMap = z.map(z.string(), UserSchema)  // Map<string, User>

// Records (object with typed keys/values)
const stringRecord = z.record(z.string())  // { [key: string]: string }
const numberRecord = z.record(z.number())  // { [key: string]: number }
const specificKeys = z.record(z.enum(["a", "b", "c"]), z.number())
// { a?: number; b?: number; c?: number }

4. Unions and Intersections

// Unions (OR - value matches one of the types)
const stringOrNumber = z.union([z.string(), z.number()])
const status = z.union([
  z.literal("pending"),
  z.literal("active"),
  z.literal("inactive")
])

// Discriminated unions (tagged unions)
const Result = z.discriminatedUnion("status", [
  z.object({ status: z.literal("success"), data: z.string() }),
  z.object({ status: z.literal("error"), error: z.string() })
])

// Usage
const success: z.infer<typeof Result> = {
  status: "success",
  data: "Hello"
}

// Enums
const Color = z.enum(["red", "green", "blue"])
type Color = z.infer<typeof Color>  // "red" | "green" | "blue"

// Native enums
enum Status {
  Pending = "PENDING",
  Active = "ACTIVE"
}
const StatusSchema = z.nativeEnum(Status)

// Intersections (AND - value must match all types)
const HasId = z.object({ id: z.string() })
const HasTimestamps = z.object({
  createdAt: z.date(),
  updatedAt: z.date()
})
const Entity = z.intersection(HasId, HasTimestamps)
// { id: string; createdAt: Date; updatedAt: Date }

// Or use .and() method
const Entity = HasId.and(HasTimestamps)

Karpathy Principle: Goal-Driven Execution - Use discriminated unions for type-safe state machines. The type system will enforce exhaustive handling of all states.

5. Refinements and Custom Validation

// Refinements (add custom validation)
const PositiveNumber = z.number().refine(
  (val) => val > 0,
  { message: "Number must be positive" }
)

// Multiple refinements
const StrongPassword = z.string()
  .min(8, "Password too short")
  .refine((val) => /[A-Z]/.test(val), "Must contain uppercase")
  .refine((val) => /[a-z]/.test(val), "Must contain lowercase")
  .refine((val) => /[0-9]/.test(val), "Must contain number")

// superRefine (access to context)
const PasswordMatch = z.object({
  password: z.string(),
  confirm: z.string()
}).superRefine((data, ctx) => {
  if (data.password !== data.confirm) {
    ctx.addIssue({
      code: z.ZodIssueCode.custom,
      path: ["confirm"],
      message: "Passwords don't match"
    })
  }
})

// Async refinements
const UniqueEmail = z.string().email().refine(
  async (email) => {
    const exists = await checkEmailExists(email)
    return !exists
  },
  { message: "Email already taken" }
)

// Use with parseAsync
const result = await UniqueEmail.parseAsync("user@example.com")

6. Transforms

// Transform values after validation
const Trimmed = z.string().transform((val) => val.trim())

const Uppercase = z.string().transform((val) => val.toUpperCase())

const NumberFromString = z.string().transform((val) => Number(val))

// Chaining transforms
const CleanedEmail = z.string()
  .transform((val) => val.trim())
  .transform((val) => val.toLowerCase())
  .refine((val) => val.includes("@"), "Invalid email")

// Transform with type change
const DateFromString = z.string().transform((val) => new Date(val))
// Input: string, Output: Date

// Complex transforms
const UserInput = z.object({
  name: z.string().transform((val) => val.trim()),
  email: z.string().transform((val) => val.toLowerCase()),
  age: z.string().transform((val) => parseInt(val, 10))
})

const user = UserInput.parse({
  name: "  Alice  ",
  email: "ALICE@EXAMPLE.COM",
  age: "30"
})
// { name: "Alice", email: "alice@example.com", age: 30 }

Karpathy Principle: Surgical Changes - Use transforms to normalize input data (trim, lowercase) early in your pipeline. Clean data at the boundary, not throughout your code.

7. Error Handling

// safeParse (returns result object)
const result = UserSchema.safeParse(data)

if (result.success) {
  console.log(result.data)  // Validated data
} else {
  console.error(result.error)  // ZodError with issues
}

// Access detailed errors
if (!result.success) {
  result.error.issues.forEach(issue => {
    console.log(issue.path)     // ["email"]
    console.log(issue.message)  // "Invalid email"
    console.log(issue.code)     // "invalid_string"
  })
}

// Format errors
import { z } from "zod"

const result = UserSchema.safeParse(invalidData)
if (!result.success) {
  // Flatten errors
  const flat = result.error.flatten()
  console.log(flat.fieldErrors)
  // { email: ["Invalid email"], age: ["Expected number, received string"] }
}

// Custom error map
const customErrorMap: z.ZodErrorMap = (issue, ctx) => {
  if (issue.code === z.ZodIssueCode.invalid_type) {
    return { message: `Expected ${issue.expected}, got ${issue.received}` }
  }
  return { message: ctx.defaultError }
}

z.setErrorMap(customErrorMap)

// Per-schema error messages
const UserSchema = z.object({
  email: z.string().email({ message: "Please enter a valid email address" }),
  age: z.number({ required_error: "Age is required" })
})

8. Type Inference

// Infer TypeScript type from schema
const UserSchema = z.object({
  name: z.string(),
  email: z.string(),
  age: z.number().optional()
})

type User = z.infer<typeof UserSchema>
// { name: string; email: string; age?: number }

// Input type (before transforms)
const TransformSchema = z.string().transform((val) => parseInt(val))

type Input = z.input<typeof TransformSchema>   // string
type Output = z.output<typeof TransformSchema> // number

// Use inferred types
function createUser(data: User): User {
  return UserSchema.parse(data)
}

// Discriminated union inference
const Result = z.discriminatedUnion("status", [
  z.object({ status: z.literal("success"), data: z.string() }),
  z.object({ status: z.literal("error"), error: z.string() })
])

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

function handleResult(result: Result) {
  if (result.status === "success") {
    console.log(result.data)  // TypeScript knows this exists
  } else {
    console.error(result.error)  // TypeScript knows this exists
  }
}

Common Patterns

1. API Request/Response Validation

// Request body validation
const CreateUserRequest = z.object({
  name: z.string().min(1).max(100),
  email: z.string().email(),
  password: z.string().min(8)
})

// Response validation
const UserResponse = z.object({
  id: z.string().uuid(),
  name: z.string(),
  email: z.string().email(),
  createdAt: z.date()
})

// API endpoint (Express example)
app.post("/users", async (req, res) => {
  // Validate request
  const result = CreateUserRequest.safeParse(req.body)
  
  if (!result.success) {
    return res.status(400).json({
      error: "Validation failed",
      details: result.error.flatten()
    })
  }
  
  // Create user with validated data
  const user = await createUser(result.data)
  
  // Validate response (ensures API contract)
  const validatedUser = UserResponse.parse(user)
  
  res.json(validatedUser)
})

2. Environment Variables Validation

// .env validation
const EnvSchema = z.object({
  NODE_ENV: z.enum(["development", "production", "test"]),
  DATABASE_URL: z.string().url(),
  PORT: z.string().transform((val) => parseInt(val, 10)),
  JWT_SECRET: z.string().min(32),
  API_KEY: z.string().optional()
})

// Parse at app startup
export const env = EnvSchema.parse(process.env)

// Now env is fully typed
console.log(env.PORT)  // number
console.log(env.NODE_ENV)  // "development" | "production" | "test"

3. Form Validation (React Hook Form)

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

const SignupSchema = z.object({
  name: z.string().min(1, "Name is required"),
  email: z.string().email("Invalid email"),
  password: z.string().min(8, "Password must be at least 8 characters"),
  confirmPassword: z.string()
}).refine((data) => data.password === data.confirmPassword, {
  message: "Passwords don't match",
  path: ["confirmPassword"]
})

type SignupForm = z.infer<typeof SignupSchema>

function SignupForm() {
  const { register, handleSubmit, formState: { errors } } = useForm<SignupForm>({
    resolver: zodResolver(SignupSchema)
  })
  
  const onSubmit = (data: SignupForm) => {
    console.log(data)  // Fully validated
  }
  
  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register("email")} />
      {errors.email && <span>{errors.email.message}</span>}
      
      <input type="password" {...register("password")} />
      {errors.password && <span>{errors.password.message}</span>}
      
      <button type="submit">Sign up</button>
    </form>
  )
}

4. Prisma Integration

import { z } from "zod"
import { Prisma } from "@prisma/client"

// Extend Prisma types with validation
const CreateUserInput = z.object({
  name: z.string().min(1),
  email: z.string().email(),
  age: z.number().int().positive().optional()
}) satisfies z.ZodType<Prisma.UserCreateInput>

// Use in API
async function createUser(input: unknown) {
  const validated = CreateUserInput.parse(input)
  
  return await prisma.user.create({
    data: validated  // Type-safe and validated
  })
}

Common Pitfalls

1. ❌ Not Using safeParse in Production

// ❌ BAD - throws error, crashes app
function handleRequest(req: Request) {
  const data = UserSchema.parse(req.body)  // Throws on invalid
}

// ✅ GOOD - graceful error handling
function handleRequest(req: Request) {
  const result = UserSchema.safeParse(req.body)
  
  if (!result.success) {
    return { error: result.error.flatten() }
  }
  
  return { data: result.data }
}

2. ❌ Forgetting Async Validation

// ❌ BAD - doesn't work with async refinements
const result = schema.safeParse(data)  // Returns wrong result

// ✅ GOOD - use parseAsync for async refinements
const result = await schema.safeParseAsync(data)

3. ❌ Over-Validating Internal Data

// ❌ BAD - validating data you already control
function processUser(user: User) {
  const validated = UserSchema.parse(user)  // Unnecessary
  // user is already typed as User
}

// ✅ GOOD - only validate at boundaries
function handleApiRequest(req: Request) {
  const validated = UserSchema.parse(req.body)  // Necessary
  processUser(validated)  // Now it's safe
}

4. ❌ Not Reusing Schemas

// ❌ BAD - duplicating schema definitions
const CreateUser = z.object({ name: z.string(), email: z.string() })
const UpdateUser = z.object({ name: z.string(), email: z.string() })

// ✅ GOOD - compose schemas
const BaseUser = z.object({ name: z.string(), email: z.string() })
const CreateUser = BaseUser.extend({ password: z.string() })
const UpdateUser = BaseUser.partial()  // All fields optional

5. ❌ Ignoring Type Inference

// ❌ BAD - manually duplicating types
interface User {
  name: string
  email: string
}
const UserSchema = z.object({ name: z.string(), email: z.string() })

// ✅ GOOD - infer type from schema (single source of truth)
const UserSchema = z.object({ name: z.string(), email: z.string() })
type User = z.infer<typeof UserSchema>

6. ❌ Using .optional() for Nullable Fields

// ❌ WRONG - optional() means field can be missing
const schema = z.object({
  bio: z.string().optional()  // bio can be undefined or missing
})

// Won't validate: { bio: null }

// ✅ CORRECT - use nullable() for null values
const schema = z.object({
  bio: z.string().nullable()  // bio can be null
})

// ✅ OR use nullish() for both
const schema = z.object({
  bio: z.string().nullish()  // bio can be null OR undefined
})

Verification Checklist

Before deploying Zod validation:

Schema Design

  • Schemas defined at module level (not inside functions)
  • Types inferred from schemas (not duplicated manually)
  • Reusable schemas composed with .extend(), .merge(), .pick(), .omit()
  • Discriminated unions used for type-safe state machines

Validation

  • Validation only at boundaries (API requests, external data)
  • safeParse() used in production code (not parse())
  • parseAsync() used for async refinements
  • Error handling implemented for failed validation

Error Messages

  • Custom error messages for user-facing fields
  • Error formatting implemented (flatten() or custom)
  • Field-level errors accessible in forms

Performance

  • Schemas cached/memoized (not recreated on every request)
  • Complex refinements avoided in hot paths
  • Async validation used sparingly

Integration with Other Skills

With TypeScript

  • Infer types from Zod schemas (single source of truth)
  • Use Zod for runtime validation, TypeScript for compile-time
  • Combine with TypeScript utility types (Partial, Pick, Omit)

With React

  • Use with React Hook Form (@hookform/resolvers/zod)
  • Validate form state on submit
  • Display field-level errors from Zod

With Next.js

  • Validate API route inputs/outputs
  • Validate Server Action inputs
  • Validate environment variables at build time

With Prisma

  • Validate Prisma inputs before database operations
  • Generate Zod schemas from Prisma schema (zod-prisma)
  • Type-safe database operations

With tRPC

  • tRPC uses Zod for input/output validation
  • Full-stack type safety from client to server
  • Share schemas between frontend and backend

References

Official Documentation

Ecosystem

Related Libraries

Meta: Skill Quality

Karpathy Principle: Think Before Coding - Zod turns runtime validation from an afterthought into a first-class design constraint. Design schemas that encode your business rules.

Completeness: 9/10 - Covers primitives, objects, arrays, unions, refinements, transforms, error handling, but not advanced coercion patterns or custom schema types.

Accuracy: 10/10 - Based on Zod 3+ official docs and production usage across thousands of projects.

Practical Examples: 10/10 - All examples are production-ready patterns used in real applications.

Maintenance: Last updated April 2026 for Zod 3+. Review annually as Zod evolves.

Known Gaps:

  • Advanced coercion patterns
  • Custom ZodType implementations
  • Zod v4 features (in beta)
  • Performance optimization for large schemas
  • Schema composition patterns for microservices

Related Skills: typescript, react-expert, nextjs, prisma, trpc-best-practices, shadcn-ui

Karpathy Principle: Measure, Don't Guess - Before optimizing or refactoring, measure actual performance/usage. Profile before you fix. Data beats intuition.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.