zod icon indicating copy to clipboard operation
zod copied to clipboard

Add an option to .refine() to abort early

Open DanielBreiner opened this issue 2 years ago • 3 comments
trafficstars

As mentioned in https://github.com/colinhacks/zod#abort-early, chained refinements are all executed, which may not be desirable in all cases. That is why superRefine has a way of aborting early - preventing further refinements from being executed. I propose adding this functionality to refine too.

DanielBreiner avatar Nov 25 '22 17:11 DanielBreiner

Since refine currently does output type narrowing, but superRefine does not (#1602), this may be extra desirable in cases like #1598 where not aborting early may lead to unexpected exceptions.

DanielBreiner avatar Nov 25 '22 17:11 DanielBreiner

Interestingly, the refinement method (not mentioned in docs) takes an IssueData parameter just like RefinementCtx.addIssue in superRefine, which allows the refinement to abort early using fatal.

const baseSchema = z.object({
  type: z.literal('Staff'),
  age: z.number(),
  driverLicenseId: z.string(),
});

const refineSchema = baseSchema
  .refine( //				<--
    ({ type, age }) => {
      switch (type) {
        case 'Staff':
          return age >= 18;
      }
    },
    // Always z.ZodIssueCode.custom
    {
      path: ['age'],
      message: 'Staff must at least 18.',
      // No fatal option
    }
  )
  // Continues validation
  .refine(
    ({ age, driverLicenseId }) =>
      driverLicenseId.slice(0, 2) === age.toString(),
    { path: ['driverLicenseId'], message: "Invalid driver's license." }
  );

const refinementSchema = baseSchema
  .refinement( //			<--
    ({ type, age }) => {
      switch (type) {
        case 'Staff':
          return age >= 18;
      }
    },
    {
      code: z.ZodIssueCode.custom, // specifiable error code
      path: ['age'],
      message: 'Staff must at least 18.',
      fatal: true, // has the fatal field
    }
  )
  .refine(
    ({ age, driverLicenseId }) =>
      driverLicenseId.slice(0, 2) === age.toString(),
    { path: ['driverLicenseId'], message: "Invalid driver's license." }
  );

const input = { type: 'Staff', age: 17, driverLicenseId: '18xxyy' };
const refineResult = refineSchema.safeParse(input);
const refinementResult = refinementSchema.safeParse(input);
console.log(!refineResult.success && refineResult.error.issues);
console.log(!refinementResult.success && refinementResult.error.issues);
[
  {
    code: 'custom',
    path: [ 'age' ],
    message: 'Staff must at least 18.'
  },
  {
    code: 'custom',
    path: [ 'driverLicenseId' ],
    message: "Invalid driver's license."
  }
]

[
  {
    code: 'custom',
    path: [ 'age' ],
    message: 'Staff must at least 18.',
    fatal: true
  }
]

DanielBreiner avatar Nov 25 '22 19:11 DanielBreiner

@DanielBreiner thanks for the PR! Good observation re: refinement method. Given that there's now

  • refine()
  • refinement()
  • superRefine()
  • possibly megaRefine() (just joking :D)

I wonder if the APIs are due for streamlining? There's a couple of differences, including that refine will take Promise<T> but refinement won't, among other details. My opinion on this is that you want

  • simple, quick refinement (refine)
  • heavy duty customizing (superRefine)

Not sure how other folks are using refine or what's the idea going forward on this @colinhacks

maxArturo avatar Nov 26 '22 13:11 maxArturo

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 Feb 24 '23 16:02 stale[bot]