Skip to main content

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)
data
Output
required
The validated output data from the schema
ctx
RefinementCtx
required
Context object with addIssue() method for adding validation errors
Returns: ZodPipe<Schema, ZodTransform<NewOutput, Output>>

Basic Usage

Simple Transformations

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

String Transformations

const uppercase = z.string().transform((val) => val.toUpperCase());

uppercase.parse("hello"); // "HELLO"

const length = z.string().transform((val) => val.length);

length.parse("hello"); // 5

Async Transformations

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"

Adding Validation During Transform

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"

Object Transformations

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 }

Chaining Transformations

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

Refinement Before Transform

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 Refine

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

Short Form

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"

Default Values with Transform

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

Transform Validation Errors

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

Build docs developers (and LLMs) love