zod icon indicating copy to clipboard operation
zod copied to clipboard

[Feature Request] Catch errors thrown in transform

Open mayorandrew opened this issue 3 years ago • 4 comments
trafficstars

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.

mayorandrew avatar Aug 04 '22 09:08 mayorandrew

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

DASPRiD avatar Aug 10 '22 13:08 DASPRiD

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.

mayorandrew avatar Aug 12 '22 15:08 mayorandrew

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

amonsosanz avatar Sep 25 '22 09:09 amonsosanz

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.

stale[bot] avatar Nov 24 '22 11:11 stale[bot]