Skip to main content

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.

Basic Transformation

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);

Transformation Signature

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:
  1. arg - The validated output value
  2. 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' }

Async Transformations

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.

Chaining Transformations

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);

Error Handling in Transformations

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.

Input vs Output Types

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'

Form Data Processing

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
// }

API Response Transformation

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
// }

Transformation Order

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.

Transformations vs Defaults

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

Transformations vs Refinements

  • 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

Performance Considerations

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

  1. Keep transformations simple - Complex logic is hard to debug
  2. Use transformations for data coercion - Converting types, normalizing formats
  3. Handle errors explicitly - Use ctx.addIssue() for validation failures
  4. Consider performance - Avoid expensive operations in transforms
  5. 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

Build docs developers (and LLMs) love