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/codex/zod-transform-refine" ~/.claude/skills/intense-visions-harness-engineering-zod-transform-refine-723e68 && rm -rf "$T"
manifest:
agents/skills/codex/zod-transform-refine/SKILL.mdsource content
Zod Transform and Refine
Transform and validate data with Zod's transform, refine, superRefine, and preprocess APIs
When to Use
- Converting parsed data to a different shape or type after validation (e.g., string to Date, string to number)
- Adding custom validation logic beyond what built-in validators support
- Implementing cross-field validation within a single schema
- Pre-processing raw input before Zod's normal validation runs (e.g., JSON.parse, trimming)
Instructions
- Use
to reshape or convert validated data — the output type can differ from the input type:.transform()
import { z } from 'zod'; // String to number const NumericStringSchema = z.string().transform((val) => parseInt(val, 10)); // Input type: string, Output type: number // String to Date const DateStringSchema = z .string() .datetime() .transform((val) => new Date(val)); // Input type: string, Output type: Date // Object reshaping const RawUserSchema = z .object({ first_name: z.string(), last_name: z.string(), email_address: z.string().email(), }) .transform(({ first_name, last_name, email_address }) => ({ displayName: `${first_name} ${last_name}`, email: email_address, }));
- Use
for single-failure custom validation — the predicate returns true to pass, false to fail:.refine()
const PasswordSchema = z .object({ password: z.string().min(8), confirm: z.string(), }) .refine((data) => data.password === data.confirm, { message: 'Passwords do not match', path: ['confirm'], // which field the error should appear on });
- Use
for multi-failure validation — you control exactly how many issues to add:.superRefine()
const RegisterSchema = z .object({ username: z.string(), password: z.string(), age: z.number(), }) .superRefine((data, ctx) => { if (data.password.length < 8) { ctx.addIssue({ code: z.ZodIssueCode.too_small, minimum: 8, type: 'string', inclusive: true, message: 'Password must be at least 8 characters', path: ['password'], }); } if (data.age < 18) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Must be 18 or older to register', path: ['age'], }); } });
- Use
to transform raw input before validation — useful for coercing from non-standard sources:z.preprocess()
// Parse JSON string before validating the object const JsonBodySchema = z.preprocess( (val) => (typeof val === 'string' ? JSON.parse(val) : val), z.object({ name: z.string(), count: z.number() }) ); // Convert empty string to undefined (common for HTML form inputs) const OptionalStringSchema = z.preprocess( (val) => (val === '' ? undefined : val), z.string().optional() ); // Coerce comma-separated string to array const CsvArraySchema = z.preprocess( (val) => (typeof val === 'string' ? val.split(',').map((s) => s.trim()) : val), z.array(z.string()) );
- Chain transforms and refinements together with
:.pipe()
const AgeFromStringSchema = z.string().transform(Number).pipe(z.number().int().min(0).max(150)); // First converts to number, then validates the number
- Use
with async operations — but then you must use.transform()
:.parseAsync()
const SlugCheckSchema = z.string().transform(async (slug) => { const exists = await db.post.findUnique({ where: { slug } }); return { slug, exists: !!exists }; }); const result = await SlugCheckSchema.parseAsync('my-post'); // { slug: 'my-post', exists: true }
Details
Input vs output types:
When you use
.transform(), the schema has different input and output types. z.infer gives the output type. To get the input type, use z.input<typeof Schema>:
const ProcessedSchema = z .object({ date: z.string().datetime(), }) .transform(({ date }) => ({ date: new Date(date), year: new Date(date).getFullYear() })); type Input = z.input<typeof ProcessedSchema>; // { date: string } type Output = z.infer<typeof ProcessedSchema>; // { date: Date; year: number }
Refine vs superRefine — when to choose:
| Scenario | Use |
|---|---|
| Single conditional check | |
| Multiple independent checks | |
Type narrowing (e.g., ) | with + return NEVER |
| Async validation | or |
Accessing sibling fields in refinement:
.refine() on an object gives access to all fields — useful for cross-field constraints:
const DateRangeSchema = z .object({ startDate: z.date(), endDate: z.date(), }) .refine((data) => data.endDate > data.startDate, { message: 'End date must be after start date', path: ['endDate'], });
Preprocess vs coerce:
z.preprocess() runs before Zod's type check. z.coerce is a Zod-managed conversion. Prefer z.coerce for numeric/boolean coercion from strings; use z.preprocess() for anything more complex.
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.