zod

📁 jpoutrin/product-forge 📅 6 days ago
1
总安装量
1
周安装量
#49433
全站排名
安装命令
npx skills add https://github.com/jpoutrin/product-forge --skill zod

Agent 安装分布

windsurf 1
amp 1
opencode 1
kimi-cli 1
github-copilot 1

Skill 文档

Zod Schema Validation

TypeScript-first schema declaration and validation library with static type inference.

Why Zod

  • Zero dependencies, 2kb gzipped
  • Works in Node.js and browsers
  • Immutable API – methods return new instances
  • Static type inference – no redundant type declarations
  • JSON Schema conversion built-in

Requirements

  • TypeScript v5.5+
  • Enable strict mode in tsconfig.json

Core Concepts

Schema Definition

Always define schemas before validation:

import { z } from "zod";

// Object schema
const UserSchema = z.object({
  id: z.string().uuid(),
  email: z.string().email(),
  age: z.number().int().positive(),
  role: z.enum(["admin", "user", "guest"]),
});

// Infer TypeScript type from schema
type User = z.infer<typeof UserSchema>;

Parsing Methods

.parse() – Throws ZodError on failure:

try {
  const user = UserSchema.parse(data);
  // user is typed as User
} catch (e) {
  if (e instanceof z.ZodError) {
    console.error(e.issues);
  }
}

.safeParse() – Returns discriminated union (preferred):

const result = UserSchema.safeParse(data);

if (result.success) {
  console.log(result.data); // typed User
} else {
  console.error(result.error.issues);
}

Async variants – Required for async refinements/transforms:

await UserSchema.parseAsync(data);
await UserSchema.safeParseAsync(data);

Primitive Types

// Basic primitives
z.string()
z.number()
z.bigint()
z.boolean()
z.date()
z.symbol()
z.undefined()
z.null()
z.void()
z.any()
z.unknown()
z.never()

// Coercion - converts input to target type
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)

String Validations

z.string()
  .min(1)              // Minimum length
  .max(255)            // Maximum length
  .length(10)          // Exact length
  .email()             // Email format
  .url()               // URL format
  .uuid()              // UUID format
  .cuid()              // CUID format
  .regex(/pattern/)    // Custom regex
  .startsWith("prefix")
  .endsWith("suffix")
  .includes("substring")
  .trim()              // Transform: trim whitespace
  .toLowerCase()       // Transform: lowercase
  .toUpperCase()       // Transform: uppercase

Number Validations

z.number()
  .int()               // Integer only
  .positive()          // > 0
  .nonnegative()       // >= 0
  .negative()          // < 0
  .nonpositive()       // <= 0
  .gt(5)               // > 5
  .gte(5)              // >= 5 (alias: .min())
  .lt(10)              // < 10
  .lte(10)             // <= 10 (alias: .max())
  .multipleOf(5)       // Divisible by 5
  .finite()            // Excludes Infinity
  .safe()              // Safe integer range

Object Schemas

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

// Make all properties optional
PersonSchema.partial();

// Make specific properties optional
PersonSchema.partial({ age: true });

// Make all properties required
PersonSchema.required();

// Pick specific properties
PersonSchema.pick({ name: true });

// Omit specific properties
PersonSchema.omit({ age: true });

// Extend with new properties
PersonSchema.extend({
  email: z.string().email(),
});

// Strict mode - reject unknown keys
PersonSchema.strict();

// Passthrough - preserve unknown keys
PersonSchema.passthrough();

// Strip unknown keys (default behavior)
PersonSchema.strip();

Arrays and Tuples

// Array of strings
z.array(z.string())
  .min(1)              // At least 1 element
  .max(10)             // At most 10 elements
  .length(5)           // Exactly 5 elements
  .nonempty();         // At least 1 element (typed)

// Alternative syntax
z.string().array();

// Tuple with fixed positions
z.tuple([
  z.string(),          // First element: string
  z.number(),          // Second element: number
]);

// Tuple with rest elements
z.tuple([z.string(), z.number()]).rest(z.boolean());

Unions and Enums

// Union types
z.union([z.string(), z.number()]);
// Shorthand
z.string().or(z.number());

// Discriminated unions (better error messages)
z.discriminatedUnion("type", [
  z.object({ type: z.literal("email"), email: z.string() }),
  z.object({ type: z.literal("phone"), phone: z.string() }),
]);

// Enum from array
z.enum(["admin", "user", "guest"]);

// Native enum
enum Role { Admin, User }
z.nativeEnum(Role);

Optional and Nullable

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

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

// Both
z.string().nullish();   // string | null | undefined

// Default values
z.string().default("anonymous");
z.string().optional().default("anonymous");

// Catch - use default on parse failure
z.string().catch("fallback");

Transforms

// Transform output type
const StringToNumber = z.string().transform((val) => parseInt(val, 10));
type Output = z.output<typeof StringToNumber>; // number

// Chain transforms
z.string()
  .trim()
  .toLowerCase()
  .transform((val) => val.split(","));

// Preprocess input before validation
z.preprocess(
  (val) => String(val),
  z.string().min(1)
);

Refinements

// Custom validation
z.string().refine(
  (val) => val.length <= 255,
  { message: "String must be 255 chars or less" }
);

// Async refinement
z.string().refine(
  async (val) => await checkUnique(val),
  { message: "Value must be unique" }
);

// Super refine for complex validations
z.object({
  password: z.string(),
  confirm: z.string(),
}).superRefine((data, ctx) => {
  if (data.password !== data.confirm) {
    ctx.addIssue({
      code: z.ZodIssueCode.custom,
      message: "Passwords don't match",
      path: ["confirm"],
    });
  }
});

Error Handling

const result = schema.safeParse(data);

if (!result.success) {
  // Access all issues
  result.error.issues.forEach((issue) => {
    console.log(issue.path);    // Field path
    console.log(issue.message); // Error message
    console.log(issue.code);    // Error code
  });

  // Flatten for form errors
  const flat = result.error.flatten();
  // { formErrors: string[], fieldErrors: { [key]: string[] } }

  // Format for display
  const formatted = result.error.format();
  // { _errors: string[], field: { _errors: string[] } }
}

Common Patterns

API Request Validation

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

// In Express/Fastify handler
const body = CreateUserRequest.parse(req.body);

Environment Variables

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

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

Form Data

const ContactForm = z.object({
  name: z.string().min(1, "Name is required"),
  email: z.string().email("Invalid email address"),
  message: z.string().min(10, "Message must be at least 10 characters"),
});

// With React Hook Form
const { register, handleSubmit } = useForm({
  resolver: zodResolver(ContactForm),
});

API Response

const ApiResponse = z.object({
  data: z.array(UserSchema),
  pagination: z.object({
    page: z.number(),
    total: z.number(),
  }),
});

const response = await fetch("/api/users");
const json = await response.json();
const validated = ApiResponse.parse(json);

JSON Schema Conversion

import { z } from "zod";

// Zod to JSON Schema
const jsonSchema = z.toJSONSchema(UserSchema);

// JSON Schema to Zod
const zodSchema = z.fromJSONSchema(jsonSchema);

Anti-Patterns

Avoid redundant type declarations

// Bad - duplicates schema
interface User {
  name: string;
  age: number;
}
const UserSchema = z.object({
  name: z.string(),
  age: z.number(),
});

// Good - infer from schema
const UserSchema = z.object({
  name: z.string(),
  age: z.number(),
});
type User = z.infer<typeof UserSchema>;

Use safeParse for user input

// Bad - throws on invalid input
const user = UserSchema.parse(userInput);

// Good - handle errors gracefully
const result = UserSchema.safeParse(userInput);
if (!result.success) {
  return { errors: result.error.flatten().fieldErrors };
}

Prefer discriminated unions

// Bad - ambiguous errors
z.union([
  z.object({ email: z.string() }),
  z.object({ phone: z.string() }),
]);

// Good - clear error messages
z.discriminatedUnion("contactType", [
  z.object({ contactType: z.literal("email"), email: z.string() }),
  z.object({ contactType: z.literal("phone"), phone: z.string() }),
]);

References