Ask questionsRFC: Transformations (e.g. casting/coercion)

⚠️ Last edit: 30 Aug

This is the proposed API for implementing data transformation functionality in Zod.

Proposed approach

No need to over complicate things. The problem to solve is how Zod should handle/support transformations from type A to type B. So I propose the creation of a new class ZodTransformer. This class is a subtype of ZodType (so you can use it like any other schema — .refine, .parse, etc).

Internally, instances of this class will have these properties

  • input: ZodType: an input schema
  • output: ZodType: an output schema
  • transformer: (arg: T['_type']) => U['_type']: a transformer function

There is only one transformer function, not a "transform chain" like in Yup (👋 @jquense). This makes it easier for Zod to statically type the function. Any sort of functional composition/piping can be done using libraries external to Zod.

You would create an instance using the ZodTransformer.create static factory method (aliased to z.transformer):


Coercing a string into a number.

const stringToNumber = z.transform(z.string(), z.number(), (data)=>parseFloat(data));
type stringToNumber = z.infer<typeof stringToNumber>; // number

stringToNumber.parse("12") // => 12 (number)


stringToNumber.input; // ZodString
stringToNumber.output; // ZodNumber

.transform method

Every ZodTransform instance will have a .transform method. This method lets you easily chain transforms, instead of requiring many nested calls to z.transform().

Every ZodType instance (the base class for all Zod schemas) will have a .transform method. This method lets you easily create a ZodTransform, using your current schema as the input:

const trimAndMultiply = z.string()
  .transform(z.string(), x =>x.trim())
  .transform(z.number(), x => parseFloat(x))
  .transform(z.number(), num => num * 5);

console.log(trimAndMultiply.parse(' 5 ')); // => 25

.toTransformer function

⚠️ Edit: This section is no longer relevant since the .transform method has been moved to the base ZodType class instead of only existing on ZodTransform.

As you can see above, the first method call is .transformer (which is a factory function that returns a ZodTransform). All subsequent calls are to .transform() (a chainable method on the ZodTransformer class).

To make the syntax for defining chains of transforms more consistent, I propose a toTransformer function:

const stringTransformer = z.transformerFromSchema(z.string());

// equivalent to
const stringTransformer = z.transformer(z.string(), z.string(), x => x);

With this you could rewrite trimAndMultiply like so:

const trimAndMultiply = z.toTransformer(z.string())
  .transform(z.string(), z.string(), x =>x.trim())
  .transform(z.number(), x => parseFloat(x))
  .transform(z.number(), num => num * 5)
  .refine(x => x > 20, 'Number is too small');

## .clean This section is now irrelevant and will now be implemented by overloading .transform()

⚠️ I really don't like the name "clean" for this; if you have any better ideas please make suggestions.

There will be redundancy if you are chaining together transforms that don't cast/coerce the type. For instance:

  .transform(z.string(), val => val.trim())
  .transform(z.string(), val => val.toLowerCase())
  .transform(z.string(), val => val.slice(0,5))

I propose a .clean method that obviates the need for the redundant z.string() calls. Instead it uses this.output as both the input and output schema of the returned ZodTransform.

  .clean(val => val.trim())
  .clean(val => val.toLowerCase())
  .clean(val => val.slice(0,5))


Transformations make the setting of default values possible for the first time.


// equivalent to
z.transformer(z.string().optional(), z.string(), x => x || "default_value");


Separate input and output types

There are some tricky bits here. Before now, there was no concept of "input types" and "output types" for a Zod schema. Every schema was only associated with one type.

Now, ZodTransformers have different types for their inputs and outputs. There are issues with this. Consider a simple function schema:

const myFunc = z.function()
  .implement(num => num > 5);


This returns a simple function that checks if the input is more than 5. As you can see the call to .implement automatically infers/enforces the argument and return types (there's no need for a type signature on num).

Now what if we switch out the input (z.number()) with stringToNumber from above?

const myFunc = z
  .implement(num => num > 5);

myFunc(8); // works
myFunc("8"); // throws

It's not really clear what should happen here. The function expects the input to be a number, but the transformer expects a string. Should myFunc("8") work?

Type guards

  1. I hadn't really considered how this will impact type guards. Like I mentioned under "Complications" in the original RFC, each schema is now associated with both an input and output type. For schemas that aren't ZodTransformers, these are the same. Type guards can only be used to verify the input type:
const stringToNumber = z.transformation(z.string(), z.number(), parseFloat);
const data = "12";

  data; // still a string

I think perhaps typeguards aren't really compatible with any sort of coercion/transformation and it might be better just to get rid of them. @kasperpeulen


Not sure how I didn't see this issue before.

Consider a union of ZodTransformers:

const transformerUnion = z.union([
  z.transformer(z.string(), z.number(), x => parseFloat(x)),
  z.transformer(z.string(), z.number().int(), x => parseInt(x)),

What should happen when you do transformerUnion.parse('12.5')? Zod would need to choose which of the transformed values to return.

One solution is to have union unions return the value from the first of its child schemas that passes transformation/validation, in the order they were passed into z.union([arg1,arg2,etc]). In the example above it would return the float, and never even execute parseInt.

Another solution is just to disallow passing transformers in unions (and any other types that would cause problems) 🤷‍♂️

Design consideration

One of my design considerations was trying to keep all data mutation/transformation fully contained within ZodTransformers. This leads to a level of verbosity that may be jarring. Instead of adding a .default() method to every Zod schema, you have to "convert" your schema into a ZodTransformer first, then you can use its .default method yourself.

Try it

Most of this has already been implemented in the alpha branch, so you can play around with it. Open to any questions or concerns with this proposal. 🤙

yarn add zod@alpha

Tagging for relevance: @krzkaczor @ivosabev @jquense @chrbala @jakeginnivan @cybervaldez @tuchk4 @escobar5


Answer questions chrbala

Returning back to the issue of unions - what if there was an explicit resolver function like resolveType in GraphQL?

Perhaps z.union could be a variadic function that by default picks the first schema that matches left-to-right, but optionally can specify a second resolver argument.

const float = z.transformer(z.string(), z.number(), x => parseFloat(x));
const int = z.transformer(z.string(), z.number().int(), x => parseInt(x));

const leftToRightNum = z.union([float, int]);
const withResolverNum = z.union([float, int], val =>
  val % 1 === 0 ? int : float

This is kind of a trivial example, but it would line up well for people who already do type resolutions with GraphQL in some way, and the explicitness is nice.


Related questions

No questions were found.
Github User Rank List