zod: refine() function gets called even when parser already failed within translate()/refine()/regex()
This is very similar to this one: https://github.com/colinhacks/zod/issues/2113
Happening in version 3.21.4
refine() function still runs even though a previous translate has already invalidated it. It’s getting passed some aribtrary object instead of the string that it expects.
const parseResult = z
.unknown()
.transform((x, ctx) => {
if (typeof x === 'string') return x;
console.log('I fail because input is a number (correct behavior)');
ctx.addIssue({
code: 'custom',
});
return z.NEVER;
})
.transform((x: string) => {
console.log("I don't get executed (correct behavior since transform above invalidated the input)");
return x;
})
.refine((x: string) => {
//BUG
console.log("I shouldn't get called but I do!!!!");
console.log(`I should be a string but I am a ${typeof x}!!!`); //'object'
console.log(x); // some sort of '{status:'aborted'} object (the value underlying z.NEVER?)
})
.safeParse(42);
console.log(`succeeded: ${parseResult.success}`); // false (correct behavior)
EDIT: looks like there are more scenarios that are causing this ‘zombie refine’ scenario. Here’s two more:
- A failure within a
refinecausing it:
import * as z from 'zod';
const parseResult = z
.string()
.refine(x => false) // force anything/everything to fail
.transform(x => {
console.log("I don't get called"); // correct
return x;
})
.refine((x: string) => {
console.log("I shouldn't get called!!!!!!");
console.log(typeof x); // string
console.log(x); // '123'
})
.safeParse('123');
console.log(`succeeded: ${parseResult.success}`); // false (correct behavior)
- a failure within a
regexcausing it:
import * as z from 'zod';
const parseResult = z
.string()
.regex(/abc/) // fail because input doesn't match regex
.transform(x => {
console.log("I don't get called"); // correct
return x;
})
.refine((x: string) => {
console.log("I shouldn't get called!!!!!!");
console.log(typeof x); // string
console.log(x); // '123'
})
.safeParse('123');
console.log(`succeeded: ${parseResult.success}`); // false (correct behavior)
These were pretty easy to stumble upon, so there are likely more out there as well.
About this issue
- Original URL
- State: open
- Created a year ago
- Reactions: 7
- Comments: 17
I can confirm:
Works around the issue, but this is still a bug since zod violates it’s own typings.
For example, without the
fatalflag toaddIssue:Either the type signature of
refineshould be changed to reflect the fact that it may receive aborted parses (e.g.{ "status": "aborted" }) (in which case the current shape of the abort message is not safe since that might be a valid instance of the type I am parsing) or it should never call future refinements after receiving aNEVER.I cannot think of a reason I would return
z.NEVERwithout wanting an early exit. I’m assuming there is some code-level architecture reason for the redundancy betweenfataland theNEVERreturn.It seems like refinement functions (such as
.regex()or.min()or things like that) throws non-fatal issue so that parsing process will continue.To stop parsing you must mark them as fatal manually. (as documented here: https://zod.dev/?id=abort-early) This rule also applies to .transform() too.
I couldn’t find any way to mark fatal the issues from functions like
.min()😕the above code outputs
if
fatal: false(default), it shows (z.NEVERis actually{ status: "aborted" }on runtime.)I am using 3.21.2 and I confirm that
refineis called even when the previousregexfails.@Darkhogg In case it’s helpful as a temporary workaround, using the
transformto do the validation still seems to work finehttps://zod.dev/?id=validating-during-transform
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.