Documentation Index
Fetch the complete documentation index at: https://mintlify.com/colinhacks/zod/llms.txt
Use this file to discover all available pages before exploring further.
Overview
Transformations allow you to modify data after validation. The .transform() method runs after all validations pass, converting the data to a different shape or type.
Use .transform() to modify validated data:
import { z } from 'zod';
const schema = z.string().transform((val) => val.length);
const result = schema.parse('hello'); // 5 (number)
From packages/zod/src/v4/classic/tests/transform.test.ts:86-91:
const r1 = z.string()
.transform((data) => data.length)
.parse('asdf');
expect(r1).toEqual(4);
From packages/zod/src/v4/classic/schemas.ts:114-116, the .transform() method signature:
transform<NewOut>(
transform: (arg: Output, ctx: RefinementCtx) => NewOut | Promise<NewOut>
): ZodPipe<this, ZodTransform<Awaited<NewOut>, Output>>
Transformations receive two parameters:
arg - The validated output value
ctx - A context object for adding issues
Common Use Cases
Type Coercion
Convert strings to numbers:
const NumberFromString = z.string().transform((val) => Number.parseFloat(val));
const result = NumberFromString.parse('42.5'); // 42.5 (number)
From packages/zod/src/v4/classic/tests/transform.test.ts:94-102:
const numToString = z.number().transform((n) => String(n));
const data = z.object({
id: numToString,
}).parse({ id: 5 });
expect(data).toEqual({ id: '5' });
String Manipulation
const Uppercase = z.string().transform((val) => val.toUpperCase());
const result = Uppercase.parse('hello'); // 'HELLO'
const TrimmedLowercase = z.string()
.transform((val) => val.trim())
.transform((val) => val.toLowerCase());
const clean = TrimmedLowercase.parse(' HELLO '); // 'hello'
Data Normalization
const PhoneNumber = z.string().transform((val) => {
// Remove all non-digit characters
return val.replace(/\D/g, '');
});
const phone = PhoneNumber.parse('(555) 123-4567'); // '5551234567'
Object Reshaping
const ApiResponse = z.object({
user_id: z.number(),
user_name: z.string(),
user_email: z.string(),
}).transform((data) => ({
id: data.user_id,
name: data.user_name,
email: data.user_email,
}));
const result = ApiResponse.parse({
user_id: 1,
user_name: 'Alice',
user_email: 'alice@example.com',
});
// { id: 1, name: 'Alice', email: 'alice@example.com' }
Transformations can be asynchronous:
const schema = z.number().transform(async (n) => {
const response = await fetch(`/api/multiply?n=${n}`);
return response.json();
});
const result = await schema.parseAsync(5);
From packages/zod/src/v4/classic/tests/transform.test.ts:104-113:
const numToString = z.number().transform(async (n) => String(n));
const data = await z.object({
id: numToString,
}).parseAsync({ id: 5 });
expect(data).toEqual({ id: '5' });
Async transformations require using parseAsync() or safeParseAsync(). Calling parse() on a schema with async transformations will throw an error.
Multiple transformations can be chained:
const stringToNumber = z.string().transform((arg) => Number.parseFloat(arg));
const doubler = stringToNumber.transform((val) => val * 2);
const result = doubler.parse('5'); // 10
From packages/zod/src/v4/classic/tests/transform.test.ts:186-192:
const stringToNumber = z.string().transform((arg) => Number.parseFloat(arg));
const doubler = stringToNumber.transform((val) => {
return val * 2;
});
expect(doubler.parse('5')).toEqual(10);
Use the context object to add validation errors:
const schema = z.string().transform((data, ctx) => {
const parsed = Number.parseInt(data);
if (Number.isNaN(parsed)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Not a valid number',
});
return z.NEVER; // Signal validation failure
}
return parsed;
});
const result = schema.safeParse('not-a-number');
// result.success === false
// result.error.issues[0].message === 'Not a valid number'
From packages/zod/src/v4/classic/tests/transform.test.ts:4-28:
const strs = ['foo', 'bar'];
const schema = z.string().transform((data, ctx) => {
const i = strs.indexOf(data);
if (i === -1) {
ctx.addIssue({
input: data,
code: 'custom',
message: `${data} is not one of our allowed strings`,
});
}
return data.length;
});
const result = schema.safeParse('asdf');
expect(result.success).toEqual(false);
Using z.NEVER
From packages/zod/src/v4/classic/tests/transform.test.ts:62-83:
const foo = z.number()
.optional()
.transform((val, ctx) => {
if (!val) {
ctx.addIssue({
input: val,
code: z.ZodIssueCode.custom,
message: 'bad',
});
return z.NEVER;
}
return val;
});
type foo = z.infer<typeof foo>; // number (not number | undefined)
const arg = foo.safeParse(undefined);
if (!arg.success) {
expect(arg.error.issues[0].message).toEqual('bad');
}
Returning z.NEVER from a transformation signals that validation should fail, and it also narrows the output type appropriately.
Transformations change the output type while keeping the input type:
const schema = z.string().transform((val) => val.length);
type Input = z.input<typeof schema>; // string
type Output = z.output<typeof schema>; // number
// The parser accepts strings
const result = schema.parse('hello'); // result is number
Practical Examples
Date Parsing
const DateFromString = z.string().transform((str) => new Date(str));
const schema = z.object({
createdAt: DateFromString,
updatedAt: DateFromString,
});
const data = schema.parse({
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-02T00:00:00Z',
});
// data.createdAt is Date
// data.updatedAt is Date
JSON Parsing
const JsonString = z.string().transform((str, ctx) => {
try {
return JSON.parse(str);
} catch (error) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Invalid JSON',
});
return z.NEVER;
}
});
const result = JsonString.parse('{"name": "Alice"}');
// { name: 'Alice' }
URL Slug Generation
const Slug = z.string().transform((val) => {
return val
.toLowerCase()
.replace(/\s+/g, '-')
.replace(/[^a-z0-9-]/g, '')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '');
});
const slug = Slug.parse('Hello World! 123'); // 'hello-world-123'
const FormSchema = z.object({
name: z.string().transform((val) => val.trim()),
email: z.string().email().transform((val) => val.toLowerCase()),
age: z.string().transform((val) => Number.parseInt(val, 10)),
newsletter: z.string().transform((val) => val === 'true'),
});
type FormInput = z.input<typeof FormSchema>;
// {
// name: string;
// email: string;
// age: string;
// newsletter: string;
// }
type FormOutput = z.output<typeof FormSchema>;
// {
// name: string;
// email: string;
// age: number;
// newsletter: boolean;
// }
const data = FormSchema.parse({
name: ' Alice ',
email: 'ALICE@EXAMPLE.COM',
age: '25',
newsletter: 'true',
});
// {
// name: 'Alice',
// email: 'alice@example.com',
// age: 25,
// newsletter: true
// }
const UserApiResponse = z.object({
id: z.number(),
first_name: z.string(),
last_name: z.string(),
email: z.string(),
created_at: z.string(),
}).transform((data) => ({
id: data.id,
name: `${data.first_name} ${data.last_name}`,
email: data.email,
createdAt: new Date(data.created_at),
}));
const user = UserApiResponse.parse({
id: 1,
first_name: 'Alice',
last_name: 'Smith',
email: 'alice@example.com',
created_at: '2024-01-01T00:00:00Z',
});
// {
// id: 1,
// name: 'Alice Smith',
// email: 'alice@example.com',
// createdAt: Date
// }
Transformations run AFTER all validations:
const schema = z.string()
.min(5) // 1. Validation
.email() // 2. Validation
.transform((val) => // 3. Transformation (runs last)
val.toLowerCase()
);
From packages/zod/src/v4/classic/tests/transform.test.ts:194-200:
const schema = z.string()
.refine(() => false) // This fails
.transform((val) => val.toUpperCase()); // This never runs
const result = schema.safeParse('asdf');
expect(result.success).toEqual(false);
If validation fails, transformations are never executed. This ensures you only transform valid data.
Understand the difference:
// Default: provides value when undefined
const withDefault = z.string().default('hello');
withDefault.parse(undefined); // 'hello'
// Transform: modifies provided value
const withTransform = z.string().transform((val) => val.toUpperCase());
withTransform.parse('hello'); // 'HELLO'
withTransform.parse(undefined); // Error: expected string
- Refinements (
.refine()) - Add validation, don’t change data
- Transformations (
.transform()) - Modify data after validation
// Refinement: validates but doesn't change the value
const refined = z.string().refine((val) => val.length > 5);
type RefinedOutput = z.output<typeof refined>; // string
// Transformation: changes the value
const transformed = z.string().transform((val) => val.length);
type TransformedOutput = z.output<typeof transformed>; // number
Transformations add overhead to parsing. For high-performance scenarios, consider whether you really need to transform during validation or if it’s better to transform separately.
// Slower: transformation during parse
const schema = z.array(z.string().transform((s) => s.toUpperCase()));
schema.parse(largeArray);
// Faster: transform after parse
const schema = z.array(z.string());
const data = schema.parse(largeArray);
const transformed = data.map((s) => s.toUpperCase());
Combining with Pipes
Transformations can be combined with pipes for complex data flows:
const transform1 = z.transform((val: string) => val.toUpperCase());
const transform2 = z.transform((val: string) => val.length);
const schema = z.string().transform(transform1).pipe(transform2);
const result = schema.parse('hello'); // 5
Best Practices
- Keep transformations simple - Complex logic is hard to debug
- Use transformations for data coercion - Converting types, normalizing formats
- Handle errors explicitly - Use
ctx.addIssue() for validation failures
- Consider performance - Avoid expensive operations in transforms
- Type safety - TypeScript will infer the output type correctly
// Good: Simple, clear transformation
const Uppercase = z.string().transform((val) => val.toUpperCase());
// Bad: Complex business logic in transformation
const Bad = z.string().transform(async (val, ctx) => {
const user = await db.users.find(val);
if (!user) {
ctx.addIssue({ code: 'custom', message: 'User not found' });
return z.NEVER;
}
const permissions = await db.permissions.get(user.id);
// ... more complex logic
return { user, permissions };
});
Common Gotchas
Async in Sync Context
const bad = z.string().transform(async (val) => val.toUpperCase());
// This will throw!
bad.parse('hello');
// Must use async parse
await bad.parseAsync('hello'); // OK
Returning Undefined
// Be careful with undefined returns
const schema = z.string().transform((val) => {
if (val === 'skip') {
return undefined; // Output type becomes string | undefined
}
return val.toUpperCase();
});
type Output = z.output<typeof schema>; // string | undefined
Next Steps