Overview
The .transform() method allows you to transform validated data into a different shape or type. Transformations run after validation succeeds, ensuring you always work with valid data.
Signature
schema.transform((data, ctx) => transformedData)
The validated output data from the schema
Context object with addIssue() method for adding validation errors
Returns: ZodPipe<Schema, ZodTransform<NewOutput, Output>>
Basic Usage
const stringToNumber = z.string().transform((val) => Number(val));
stringToNumber.parse("42"); // 42
type Input = z.input<typeof stringToNumber>; // string
type Output = z.output<typeof stringToNumber>; // number
const uppercase = z.string().transform((val) => val.toUpperCase());
uppercase.parse("hello"); // "HELLO"
const length = z.string().transform((val) => val.length);
length.parse("hello"); // 5
Transforms can be asynchronous:
const fetchUser = z.number().transform(async (id) => {
const response = await fetch(`/api/users/${id}`);
return response.json();
});
const user = await fetchUser.parseAsync(123);
Async transforms must use parseAsync() or safeParseAsync(). Calling parse() with an async transform will throw an error.
const asyncSchema = z.number().transform(async (n) => String(n));
// ❌ This will throw
try {
asyncSchema.parse(42);
} catch (error) {
console.log("Cannot use parse() with async transforms");
}
// ✅ This works
await asyncSchema.parseAsync(42); // "42"
Use the context object to add validation errors:
const allowedStrings = ["foo", "bar", "baz"];
const schema = z.string().transform((data, ctx) => {
if (!allowedStrings.includes(data)) {
ctx.addIssue({
code: "custom",
message: `${data} is not an allowed value`,
});
}
return data.length;
});
const result = schema.safeParse("invalid");
if (!result.success) {
console.log(result.error.issues[0].message);
// "invalid is not an allowed value"
}
Using z.NEVER
Return z.NEVER to exclude a value from the output type:
const schema = z.number()
.optional()
.transform((val, ctx) => {
if (!val) {
ctx.addIssue({
code: "custom",
message: "Value is required",
});
return z.NEVER;
}
return val;
});
type Output = z.output<typeof schema>; // number (not number | undefined)
const result = schema.safeParse(undefined);
// Error: "Value is required"
Type Coercion in Objects
const schema = z.object({
id: z.number().transform(String),
createdAt: z.string().transform((s) => new Date(s)),
});
const result = schema.parse({
id: 123,
createdAt: "2024-01-01"
});
type Input = z.input<typeof schema>;
// { id: number; createdAt: string }
type Output = z.output<typeof schema>;
// { id: string; createdAt: Date }
Reshaping Objects
const apiSchema = z.object({
user_id: z.number(),
user_name: z.string(),
created_at: z.string(),
}).transform((data) => ({
id: data.user_id,
name: data.user_name,
createdAt: new Date(data.created_at),
}));
const result = apiSchema.parse({
user_id: 1,
user_name: "Alice",
created_at: "2024-01-01"
});
// { id: 1, name: "Alice", createdAt: Date }
You can chain multiple transformations:
const schema = z.string()
.transform((s) => Number(s))
.transform((n) => n * 2)
.transform((n) => String(n));
schema.parse("5"); // "10"
Combining with Refinements
Validate before transforming:
const schema = z.string()
.refine((s) => !isNaN(Number(s)), "Must be numeric")
.transform(Number);
schema.parse("42"); // 42
schema.parse("hello"); // Error: "Must be numeric"
Transform, then validate the result:
const schema = z.string()
.transform(Number)
.refine((n) => n > 0, "Must be positive");
schema.parse("42"); // 42
schema.parse("-5"); // Error: "Must be positive"
Short-Circuiting on Errors
By default, transforms don’t run if validation fails:
const schema = z.string()
.refine(() => false, "Validation failed")
.transform((val) => {
console.log("This never runs");
return val.toUpperCase();
});
const result = schema.safeParse("hello");
// Only validation error, transform is skipped
Context Methods
ctx.addIssue()
Add a validation error:
schema.transform((data, ctx) => {
ctx.addIssue({
code: "custom",
message: "Custom error message",
path: ["field"], // Optional path
fatal: true, // Optional: stop validation
});
return data;
});
Use a string for simple custom messages:
schema.transform((data, ctx) => {
ctx.addIssue("Something went wrong");
return data;
});
Continuing After Errors
By default, adding an issue stops further validation. Set continue: true to continue:
const schema = z.string()
.transform((val, ctx) => {
ctx.addIssue({
code: "custom",
message: "Warning: non-critical issue",
continue: true, // Allow validation to continue
});
return val;
})
.refine(() => false, "Another check");
const result = schema.safeParse("test");
// Both errors are reported
Common Patterns
JSON Parsing
const jsonSchema = z.string().transform((str, ctx) => {
try {
return JSON.parse(str);
} catch (e) {
ctx.addIssue({
code: "custom",
message: "Invalid JSON",
});
return z.NEVER;
}
});
const schema = jsonSchema.pipe(
z.object({
name: z.string(),
age: z.number(),
})
);
Trimming and Normalizing
const normalizedString = z.string()
.transform((s) => s.trim())
.transform((s) => s.toLowerCase())
.refine((s) => s.length > 0, "Cannot be empty after trimming");
normalizedString.parse(" HELLO "); // "hello"
const withDefault = z.string()
.optional()
.transform((val) => val ?? "default");
withDefault.parse(undefined); // "default"
withDefault.parse("hello"); // "hello"
Computing Derived Fields
const personSchema = z.object({
firstName: z.string(),
lastName: z.string(),
}).transform((data) => ({
...data,
fullName: `${data.firstName} ${data.lastName}`,
}));
const result = personSchema.parse({
firstName: "John",
lastName: "Doe"
});
// { firstName: "John", lastName: "Doe", fullName: "John Doe" }
Async Data Enrichment
const enrichedUser = z.object({
id: z.number(),
email: z.string().email(),
}).transform(async (user) => {
const profile = await fetchProfile(user.id);
const posts = await fetchPosts(user.id);
return {
...user,
profile,
posts,
};
});
const user = await enrichedUser.parseAsync({
id: 1,
email: "[email protected]"
});
Encoding Errors
Transformations are unidirectional by default. Use .encode() will throw:
const schema = z.string().transform((val) => val.length);
try {
z.encode(schema, 5);
} catch (error) {
console.log(error.message);
// "Encountered unidirectional transform during encode: ZodTransform"
}
For bidirectional transformations, use z.codec().
Type Inference
Transforms affect type inference:
const schema = z.object({
stringToNumber: z.string().transform(Number),
numberToString: z.number().transform(String),
});
type Input = z.input<typeof schema>;
// { stringToNumber: string; numberToString: number }
type Output = z.output<typeof schema>;
// { stringToNumber: number; numberToString: string }
Error Handling Examples
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");
if (!result.success) {
console.log(result.error.issues);
// [
// {
// code: "custom",
// message: "asdf is not one of our allowed strings",
// path: []
// }
// ]
}
Best Practices
Transform after validation
Always validate your data before transforming it. Transformations assume valid input.
Use meaningful transformations
Transformations should have a clear purpose. Don’t use them for side effects.
Handle errors gracefully
Use ctx.addIssue() for expected error cases. Let unexpected errors throw naturally.
Consider performance
Async transforms and complex transformations can impact performance. Cache results when possible.
Type safety matters
Let TypeScript infer types when possible. Explicit type annotations can catch transformation errors.
See Also
- Pipe - Chain schemas together
- Refine - Custom validation
- Coerce - Type coercion
- Codec - Bidirectional transformations