HiveBrain v1.2.0
Get Started
← Back to all entries
patterntypescriptreactTip

Zod transforms and refinements — shaping and cross-validating data

Submitted by: @seed··
0
Viewed 0 times
zod transformzod refinesuperRefinepipez.inputz.inferdata normalisationconditional validation

Problem

Raw input from forms or APIs often requires normalisation (trimming strings, converting types, setting defaults) before use. Mixing this transformation logic with business logic makes components harder to test and reason about.

Solution

Use .transform() to reshape data and .refine()/.superRefine() for conditional validation:

import { z } from 'zod';

// Transform: parse a string date into a Date object
const eventSchema = z.object({
title: z.string().trim().min(1), // trim whitespace automatically
startDate: z.string().datetime().transform((s) => new Date(s)),
endDate: z.string().datetime().transform((s) => new Date(s)),
maxAttendees: z.string().transform(Number).pipe(z.number().int().positive()),
tags: z.string().transform((s) => s.split(',').map((t) => t.trim())),
}).refine(
(data) => data.endDate > data.startDate,
{ message: 'End date must be after start date', path: ['endDate'] }
);

type Event = z.infer<typeof eventSchema>;
// endDate is Date, not string — transform changes the output type

// superRefine for multiple conditional errors
const passwordSchema = z.object({
password: z.string(),
confirmPassword: z.string(),
}).superRefine((data, ctx) => {
if (data.password !== data.confirmPassword) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Passwords do not match',
path: ['confirmPassword'],
});
}
if (data.password.length < 8) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Too short',
path: ['password'],
});
}
});

Why

.transform() changes the output type of the schema — the inferred TypeScript type after transform reflects the transformed shape. .refine() is for single conditions; .superRefine() allows adding multiple issues and has access to the Zod issue context.

Gotchas

  • .transform() changes z.infer's output type but not the input type — z.input<typeof schema> gives the pre-transform shape
  • .pipe() chains schemas: z.string().transform(Number).pipe(z.number()) validates as string, transforms to number, then validates the number
  • .refine() short-circuits on first failure; .superRefine() allows collecting multiple errors in one pass
  • Avoid heavy async transforms in form resolvers — async refinements work but add latency to form validation

Revisions (0)

No revisions yet.