zod: Refine validations on object definitions don't get triggered until all fields in the object exist.
Applies to zod v3. I have not tried the same scenario on older versions.
I have two dates, startDate and endDate and I want to verify that the former is always earlier than the latter. I tried doing something like this:
z.object({
name: z.string(),
startDate: z.date(),
endDate: z.date(),
})
.refine((data) => checkValidDates(data.startDate, data.endDate), 'Start date must be earlier than End date.')
The problem is that the .refine() function defined on the object type doesn’t get triggered until the all fields in the object are defined. So, imagine you have a form and user entered both startDate and endDate first, but in a wrong order. At this point, the validations for these dates do not trigger because user has not entered name. Then, user goes ahead enters name and user now sees the validations for the dates kicking in. This is very awkward experience.
I can see that in this issue #61, the example adds a .partial() to get around this problem, but this really isn’t a solution because all of these fields are required in my case. It’d be nice if the .refine function defined on each individual field had access to the current form values, but it has access to its own value only. Any ideas, or did I miss something?
About this issue
- Original URL
- State: closed
- Created 3 years ago
- Reactions: 56
- Comments: 47
Let me reiterate the issue in more detail.
It may be by design that
refinevalidations on az.objectdo not trigger until all fields in the object are filled in. For example, this is probably desired behavior when validating a password and a password confirmation. In this case, we’d expect therefinevalidations to trigger once both fields are filled in. However, this is only true if those are the only fields being validated. If this example had several more fields, then we still want to trigger the validation as soon as those two fields are filled in, not when all fields are provided.One solution is to create a nested object schema and use
refineon that. e.g.Sure, this works, but what if there’s another field that depends on start and end dates? In my scenario, the start and end date are the dates of an event. I also have a due date field which specifies when an event registration is due. So, I need to make sure it’s earlier than the start date. Now, I need to wait until start and end dates are both filled in before using a refine validation. But, what I want is to trigger the validation as soon as start and due date are filled in. zod cannot currently handle this if I’m correct. Also, doing this breaks the structure, so it doesn’t play nice with Blitz.js.
My suggestion is to have something like yup.when which gives you access to values of other fields. To enable this, the
refinefunction may also give you back a function that retrieves the value of a specified field. For example:This is my suggestion. By moving the refine into the field schema, then we can makes sure it gets triggered as soon as that field is modified. There’s also no need to create a nested object schema. If there’s an existing solution, then that’s great. Please let met know.
I hope I made my point clear enough. I’m not sure if this is technically possible with how zod is written, but this feels like such a basic scenario, so it’d be great support this.
Solution
Use
z.intersectionon two objects:Hope this helps someone
Really sad about this behaviour, tbh. Idk if I’m being too shallow in my thoughts, but to me it seems you could just execute the refinements even if the schema validation fails, then this problem would be solved. But ok, let’s try to make workarounds for this
I’m currently in the process of testing Zod to migrate from Yup. I love it so far, but the issue is also an irritant for us
I’m currently facing the same problem as everyone has mentioned already, refine/superRefine function is not getting called or triggered, I was going through the docs to see why it is happening but nth was mentioned there, if thats the expected behaviour then please update the docs, otherwise I do hope this interesting behaviour is actually fixed.
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.
Hi guys! Just came across the same issue, and I think I found a nicer way to handle it, without intersect and other stuff, but subschemas 😄
Now password validation is confirmed when both passwords are available, and email validation is done when the profile is updated (could be limited to only email fields ofcourse…
My MUI jsx looks like this now:
I wish I could’ve stumbled upon this issue earlier using zod, should’ve gone the yup way 😕
Indeed splitting the schema does solve the problem, but for complex schemas it isn’t a scalable solution. I did it in my project but I sincerely want a solution like yup has. Ok, you can say “use yup” but zod has other features and benefits that I like.
I had to split my forms into 5, 6 schemas… And if you need to get a value from another form for some reason, it just makes things difficult…
It’s a solution, I’m using it, but I really don’t like it…
I’ve been using zod, it’s great… However, I’m changing a full yup project to zod and this behavior is driving me crazy. A ton of rework splitting up schema just because of it. @colinhacks any plans to allow refinements even though the schema’s definition has not been passed?
Just come across this 2 years after it was opened, and also find that this is an annoyance for us.
We’re using
react-hook-formwhich takes Zod in its stride, but in this case with have an object for the data with many properties including a start and end date, using refine we can validate that the end date is after the start, but this validation doesn’t run and present itself until all other checks in the non-refine step have been resolved.If we had someway to reference other data, that would be perfect:
But this is not made available, and so it seems we are doomed to post-validation refine, or splitting our Zod up into pieces
What I mentioned related to the form is just an example. My point is still that
refinedoes not get triggered until all the fields in theobjectdefinition is passed in. Thus, any form handling processes will have this issue because therefinevalidations are never surfaced.I created a little code sandbox for you to try. https://codesandbox.io/s/goofy-fog-evved?file=/src/App.js
Here, this is what I’m doing in the codesandbox.
For this, I expect these validation results to be produced: 1) “name” is required and 2) “age” must be greater than or equal to 0. However, what I see is only “name” is required.
I don’t think this is similar to #690, a use case for triggering
refinebefore filling all the form fields or having access to the other values onrefineis where we’re having a form in a wizard, where we need to do validation step by step. If in the first step we need to conditionally validate field_2 based on the value of field_1 (both on step 1) we can’t do it since fields from the other steps are not yet filled.We need
refineto work without any hacks, @colinhacks please find a solution to this bug!A version that embraces the partial nature of the form: TypeScript Playground
I came up with this helper that treats the data as if its a zod any but then returns the type that was passed into it. Not sure if theres any differences between z.any and z.*, but all I need from this is refine before using the product as a child validation unit or for final validation. My use-case for this is validating forms with react hook forms, where the validator gets run on field change and the data partially fills the schema.
However, it would be nice to have some sort of flag that you can pass into any zod unit that validates regardless of the intrinsic validation result.
@mosnamarco Ah THANK YOU, I was loosing my mind over this 😬❤️
Update
For those who are still looking as to why this is, the .refine() won’t run if it fails to validate the objects before it. The solution of splitting the schema into two solves this because it isolates the validation to only those specific objects, effectively making them pseudo concurrent.
Just like a traffic lane, if one lane is in traffic (assuming you have two lanes on the same direction), just switch lanes, in our case, we create one.
I solved this problem with just adding this code at the end of the schema definition.
.and(z.object({}));Any updates on clean ways to work around this? this thing is driving me crazy, how hard can front-end validation be… Its unthinkable why this would be a problem, since it’s the nature of a form to have undefined fields if the’re untouched.
This is a non-starter for us to switch from Yup to Zod. The use case that we have might be addressed by https://github.com/colinhacks/zod/issues/1394
Yeah, ok. That works out nicely. I did not think of using
z.inferthat way.Though, I still think this should be somehow built-in to zod. I’m happy with this workaround now, but I believe supporting it would make zod even easier to work with. Well, I’ll let the community decide… Anyways, thanks a lot for the help.
I also encountered a problem when switching from yup to zod, when I couldn’t normally “turn on/off” fields depending on the value of one field.
refinedoesn’t help at all, since it doesn’t work until all the fields are filled in. Yup in this regard is much more convenient than zod, it haswhenand with its help you can solve all your problems in 2 minutes. @colinhacks , please add functionality similar towhenin yup, I’m already getting a headache from how inflexible zod is in this regard.@scotttrinh I don’t think this is a working solution. The updated example using
.extendis now identical (?) to my first example.I don’t think is a form-related problem. When I validate API requests in a server-to-server communication I’d like to have the same behaviour: I’d like to see as many validation errors as possible.
Would it be possible to introduce some kind of “select” API instead of splitting schemas (which doesn’t seem to be always possible). Could an API like this theoretically work (using https://github.com/colinhacks/zod/issues/479#issuecomment-1536233005 as an example)?:
First of all, thank you @Yin117 , this worked as expected! 😄
This is the solution I used, in my case I’m using it to validate password and password confirm. I’m currently using it in my nextjs application. here is what my solution looks like.
Note that I’m using it inside a page.tsx component, this is declared just above the
export default functionyou can make all fields optional and at the end of your schema you can use .refine/.superRefine with something like this:
My two cents:
refineandtransformboth assume that the object you’re refining or transforming passes the initial schema. If it does not, than you need to translate your object into that schema or else the type oftransformorrefinecan’t be correct at that point. In the partial form case, you might want to make your schema.partial, but then you’ll need to deal with that in yourrefinetype, which makes sense to me.You could be explicit bout what you are trying to do and make two schemas: one for the form and one for the model. Something like this? TypeScript Playground
This was amazing @Mjtlittle , thanks!
facing same issue, when using with react hook form and try to build multi step form. as refine only works on form submit, My error on the previous step are not visible as I am on the final step.
really need field level refine with full data. or just execute refine when calling trigger([“some field”])
chiming in on the intersection workaround, I found using and was a little more ergonomic to use .and to chain things together. In theory I thought you’d be able to use Array.reduce to chain a list of schemas into one, but in practice I wasn’t able to get the types to work.
Ultimately, I see the value of
zod(and other similar libraries) as being something that can turn anunknowninto a concrete type that I can reliably depend on at runtime. Being such a general-purpose tool means that it’s less specifically adapted to things like form validation, or automatic ORM generation, or any other such specific use-case for a runtime representation of your types. But, I believe that there is an opportunity to write abstractions that leveragezodto provide a nice experience for form validation, or ORM tooling, or GraphQL generation, etc. etc. I suspect we’ll see form libraries (e.g. formik, et al) start to help with that abstraction aszodgains more users, but until then, there will be some work to create these abstractions yourself for your use cases, I think.Somewhat related to this, there is a PR in the works that adds a concept of “pre-processing” (https://github.com/colinhacks/zod/pull/468) that allows transforming the input before the schema is checked that might help a bit with this, but not much more elegant (imo) than the solution I outlined above I think.
Glad to help!
Hi, thanks for you two for taking time for this issue. Your example gets the job done, but there’s still one issue in the real world scenarios. If there are separate schemas, then I’ll need to add the same
refines to both of them. In my case, I at least have threerefines to do as I showed earlier. (My actual schema has much more fields and I have several morerefines to do.) To avoid duplication, I created a function to add them to the base schema, but I cannot get the TypeScript typing to play nice with this approach. The following is the best I came up with my scenario (Note: I added an optional field in this example, since usingrequiredas in your example will not work for schema with optional fields.)I don’t think there’s a better way to handle this?
What I mentioned related forms are still just examples. It is not really what I want to discuss. (Well, if you are curious why missing keys in those “forms” do not have
""as the default value, then I can explain a bit more. Blitz.js which is also a sponsor of this project uses React Final Form and zod as a recommended way to perform validations on browser and server side. The same zod schema is used on both sides. React Final Form does not give""for empty input fields. So, user doesn’t get any custom validation results set on az.objectuntil all fields are provided. Again, this is just an example. It doesn’t matter where the validation takes place.)Currently, zod doesn’t have a good way to do validations against more than one field and that’s what I’m trying to point out here.
I also think this is a not a corner case. It’s not uncommon to have fields that depend on some other fields (e.g. password and password confirmation, start/end dates, etc.), whether it’s a form or any other json data, and we usually want to surface the error as soon as we detect them.
The suggestion to use
""for missing key sounds like a dirty hack. Doing so means modifying the data that’s being validated. Well, I could make a deep clone of the data and keep the original one intact, but I still need to manually go through each field and add an empty string. I don’t think I should be doing that just to allow multi-field validations.I skimmed through the source code a bit today, but isn’t it possible to hold a reference to the original data inside the
ZodTypewhen it callsparse? Then, we could pass a function to retrieve a specific value from it (likegetFieldin my example above) or a deep clone of the data into therefinemethod?@ivan-kleshnin I went through this again, but I still think what you mentioned contradicts with my findings. It’d be great if you could let me know how zod solves this issue.