colinhacks / zod

TypeScript-first schema validation with static type inference
https://zod.dev
MIT License
33k stars 1.14k forks source link

[Feature Request] Catch errors thrown in transform #1309

Closed mayorandrew closed 1 year ago

mayorandrew commented 2 years ago

README says that transform should not throw errors. This makes it less convenient to use for two reasons:

  1. If the code can throw errors, we need to ensure that they are caught and transformed to ctx.addIssue calls with fatal: true
  2. There is often no meaningful value to return in case the error happens, so for zod to infer the output type correctly, some hacks are needed

For example, I wrote this utility helper in my applicaiton to handle this problem:

export const zodCatch =
  <Input, Result>(fn: (input: Input) => Result) =>
  (input: Input, ctx: RefinementCtx) => {
    try {
      return fn(input);
    } catch (error) {
      if (error instanceof Error) {
        ctx.addIssue({
          fatal: true,
          code: ZodIssueCode.custom,
          message: error.message,
          params: {
            ...error,
            name: error.name,
          },
        });
      } else {
        ctx.addIssue({
          fatal: true,
          code: ZodIssueCode.custom,
          message: String(error),
        });
      }

      // Hack for zod to infer type correctly
      // this value won't be used because we added fatal issues to ctx
      return undefined as any as Result;
    }
  };

I use it like this:

const schema = z.string().transform( zodCatch((v) => JSON.parse(v)) );
const parsed = schema.parse("invalid json");

Although this helper solves the problem for me, it would be nice if something like this could be included in zod itself. If catching all errors is too much (it would likely be a breaking change), maybe it can only catch instances of a specific error class. That would still be beneficial because it would eliminate the need for a return type hack.

DASPRiD commented 2 years ago

I agree that transform should have the option to emit errors. Right now I have to basically run the same quote twice, first in a refinement and then again in the transform. E.g. to get a DecimalJs value:

z.string()
  .refine(value => {
    try { new DecimalJs(value) } catch { return false; }
    return true;
  })
  .transform(value => new DecimalJs(value));

It would be ideal if the transform function would be allowed to throw exceptions, and either use the exception message as error, or optionally to pass a secondary argument to the transform function which then will be used as value.

Thus the above code could be represented instead as:

z.string().transform(value => new DecimalJs(value), 'Invalid decimal');
mayorandrew commented 2 years ago

Right now I have to basically run the same quote twice, first in a refinement and then again in the transform

@DASPRiD in the latest versions of zod you don't have to do this twice because transform provides the ctx argument:

z.string()
  .transform((value, ctx) => {
    try { 
      return new DecimalJs(value);
    } catch (error) {
      ctx.addIssue({
        fatal: true,
        code: ZodIssueCode.custom,
        message: error.message,
        params: { ...error, name: error.name }
      })
    }

    // This is a hack to get proper type inference
    return undefined as unknown as DecimalJs;
  });

Nevertheless, having native support for throws in transform would make the syntax much simpler.

amonsosanz commented 1 year ago
// This is a hack to get proper type inference
return undefined as unknown as DecimalJs;

I see we can now return z.NEVER to avoid the casting hack.

stale[bot] commented 1 year ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.