zod icon indicating copy to clipboard operation
zod copied to clipboard

[V4] ZodSafeParseResult only keeps the output type of the schema

Open MarinoBenassai opened this issue 3 months ago • 3 comments

When using safeParse, the returned ZodSafeParseResult only keeps track of the output type of the schema. As a result, when trying to format errors with treeifyError, the resulting ZodErrorTree uses the type of the schema output instead of the input. This leads to incorrect typing when piping a schema to a transform:

import { z, treeifyError } from 'zod';

type A = { a: number };
type B = { b: string };

const baseSchema = z.object({
  a: z.number().max(9),
});

const transform = z.transform<A, B>(({ a }) => ({
  b: a.toString(),
}));

const pipe = baseSchema.pipe(transform);

const errors = pipe.safeParse({ a: 10 })?.error;

if (errors) {
  console.log(treeifyError(errors).properties?.a); // Typescript error but the property exists
  console.log(treeifyError(errors).properties?.b); // No Typescript error but the property does not exist
}

MarinoBenassai avatar Sep 02 '25 13:09 MarinoBenassai

Yeah, it's a known unsoundness. There's nothing Zod can really do here. z.treeifyError is a convenience method, and it works in the 98% case. You can do get the generic like so to prevent Zod from assuming the output type:

z.treeifyError<unknown>(errors)

You can also pass in a different type entirely (the input type, etc) but you'll have to do some casting on errors.

colinhacks avatar Sep 11 '25 04:09 colinhacks

it works in the 98% case

The docs for .transform says that "Piping some schema into a transform is a common pattern", and the bug occurs any times this is done, so is it that rare ?

There's nothing Zod can really do here.

This used to work in Zod v3 because SafeParseReturnType kept both the type of the input and the output. Is it no longer possible with the v4 architecture to have for example ZodSafeParseResult<Input, Output> ?

MarinoBenassai avatar Sep 12 '25 09:09 MarinoBenassai

I have the same issue. This is a bug in the ZodSafeParseResult type.

export type ZodSafeParseResult<T> = ZodSafeParseSuccess<T> | ZodSafeParseError<T>;

We should have separate input and output types in the safe parse result.

export type ZodSafeParseResult<O, I> = ZodSafeParseSuccess<O> | ZodSafeParseError<I>;

May we please get this fixed?

aaditmshah-commversion avatar Nov 26 '25 13:11 aaditmshah-commversion