install
source · Clone the upstream repo
git clone https://github.com/Intense-Visions/harness-engineering
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/Intense-Visions/harness-engineering "$T" && mkdir -p ~/.claude/skills && cp -r "$T/agents/skills/claude-code/zod-schema-definition" ~/.claude/skills/intense-visions-harness-engineering-zod-schema-definition-4adaaf && rm -rf "$T"
manifest:
agents/skills/claude-code/zod-schema-definition/SKILL.mdsource content
Zod Schema Definition
Define runtime-validated TypeScript schemas with z.object, primitives, enums, literals, and schema composition
When to Use
- You need runtime validation that also produces TypeScript types (no manual interface duplication)
- You are receiving untrusted data: API responses, form submissions, environment variables, query params
- You want a single source of truth for both runtime validation and static typing
- You are starting a new data model and want to define it as a Zod schema first
Instructions
- Install Zod:
npm install zod - Import the
namespace:zimport { z } from 'zod' - Define primitive fields — choose the right primitive for each field:
,z.string()
,z.number()
,z.boolean()
,z.date()
,z.bigint()z.symbol()
,z.undefined()
,z.null()
,z.void()
,z.any()
,z.unknown()z.never()
- Use
to compose fields into a schema object — each key maps to a Zod type:z.object()
import { z } from 'zod'; const UserSchema = z.object({ id: z.string().uuid(), name: z.string().min(1), email: z.string().email(), age: z.number().int().positive().optional(), role: z.enum(['admin', 'editor', 'viewer']), createdAt: z.date(), });
- Use
for a fixed set of string literals — prefer overz.enum()
for simple cases:z.union([z.literal(...)])
const StatusSchema = z.enum(['active', 'inactive', 'pending']); type Status = z.infer<typeof StatusSchema>; // 'active' | 'inactive' | 'pending'
- Use
for exact value matching:z.literal()
const TrueSchema = z.literal(true); const FortyTwoSchema = z.literal(42); const HelloSchema = z.literal('hello');
- Make fields optional or nullable explicitly:
const ProfileSchema = z.object({ bio: z.string().optional(), // string | undefined avatar: z.string().nullable(), // string | null website: z.string().nullish(), // string | null | undefined });
- Compose schemas by nesting them — use schema references, not re-definition:
const AddressSchema = z.object({ street: z.string(), city: z.string(), zip: z.string().regex(/^\d{5}$/), }); const OrderSchema = z.object({ id: z.string().uuid(), shippingAddress: AddressSchema, billingAddress: AddressSchema.optional(), });
- Parse input data — use
to throw on failure,.parse()
to handle errors gracefully:.safeParse()
// Throws ZodError if invalid const user = UserSchema.parse(rawData); // Returns { success: true, data } or { success: false, error } const result = UserSchema.safeParse(rawData); if (result.success) { console.log(result.data); // fully typed } else { console.error(result.error.issues); }
- Export both the schema and its inferred type together:
export const UserSchema = z.object({ ... }) export type User = z.infer<typeof UserSchema>
Details
Zod schemas are composable objects — they are not just validators, they are type constructors. Every schema carries both a runtime validator and a TypeScript type. The
z.infer<typeof Schema> pattern eliminates the dual-maintenance problem of keeping an interface and a validator in sync.
Schema composition strategies:
- Flat schemas — all fields in one
. Good for simple models.z.object() - Nested schemas — reference child schemas by variable. Better for shared sub-shapes.
- Extended schemas — use
to add fields without re-defining the base. See.extend()
.zod-object-patterns
Primitive coercion:
Zod does not coerce by default.
z.number().parse("42") throws. Use z.coerce.number() when input may be a string (e.g., URL query params, form fields):
const PageSchema = z.object({ page: z.coerce.number().int().positive().default(1), limit: z.coerce.number().int().min(1).max(100).default(20), });
Default values:
const ConfigSchema = z.object({ timeout: z.number().default(5000), retries: z.number().default(3), debug: z.boolean().default(false), });
Recursive schemas:
For tree structures, use
z.lazy():
type Category = { name: string; subcategories: Category[]; }; const CategorySchema: z.ZodType<Category> = z.lazy(() => z.object({ name: z.string(), subcategories: z.array(CategorySchema), }) );
When NOT to use Zod schema definition directly:
- For large union types with many variants — prefer
(seez.discriminatedUnion()
)zod-union-discriminated - For complex string patterns — use dedicated string validators (see
)zod-string-validation - For full object manipulation — use object-level methods (see
)zod-object-patterns
Source
Process
- Read the instructions and examples in this document.
- Apply the patterns to your implementation, adapting to your specific context.
- Verify your implementation against the details and edge cases listed above.
Harness Integration
- Type: knowledge — this skill is a reference document, not a procedural workflow.
- No tools or state — consumed as context by other skills and agents.
Success Criteria
- The patterns described in this document are applied correctly in the implementation.
- Edge cases and anti-patterns listed in this document are avoided.