zod: [question] is it possible to stop parsing on the first error?

I’m using Zod to validate HTTP payloads, some of which may be expensive to validate. Is it possible for schema.parse() and their friends to stop on the first error and return a single ZodIssue?

About this issue

  • Original URL
  • State: open
  • Created 2 years ago
  • Reactions: 21
  • Comments: 27 (1 by maintainers)

Most upvoted comments

Zod is designed to surface as many errors as possible, so not really. Some sort of abortEarly mode is certainly possible but I’m not convinced that it’s worth the increase in complexity - it’s not a very common use case.

I agree that the design is structured to expose as many errors as possible - however, I disagree that it’s not a common use case to need to halt mid-validation.

A pretty common use-case is form-validation utilizing database queries. Example: if I want to validate a simple email/password form, I can string().email(). But if want to combine that with the database query to keep all my errors in sync and make it string().email().superRefine() I can only halt/abort after the first db query I run in the refinement - and if any previous validators in the chain failed - I’m running the db query for nothing - and either with potentially unsecure data or I need to re-validate in the refinement to ensure it’s good to go. The other alternative would be to use a refinement on the entire data object, but the same issue would persist - I’d be running all the validators and the first db query even if the entry data was invalid.

Just my two cents, but it would be a major improvement in an already amazing package. I can see huge value in a simple bail() function - a la https://github.com/colinhacks/zod/issues/1606#issuecomment-1381702201 - that would halt and return (akin to the fatal & Z.NEVER in refinements).

Thanks again, @colinhacks, for Zod - it’s a great resource!

Zod is designed to surface as many errors as possible, so not really. Some sort of abortEarly mode is certainly possible but I’m not convinced that it’s worth the increase in complexity - it’s not a very common use case.

In form validation, it’s more common to display only one error for a field and not to throw multiple errors at our users.

For example:

  • We have string().min(1, "required").email() schema for a field.
  • User submits a form with an empty string.
  • In most cases, we show only the first error to the user, which is "required" in this case.
  • Showing "required" and "invalid email" is kind of redundant.

Also, in this kind of form validation, there’s no point in validating if an empty string is an email.

When we add a few more “expensive” rules in the validation chain, for example, we ping a server, the issue becomes more apparent.

I can understand that zod is (maybe) not intended to cover these cases but as @valentsea mentioned above, it would be a significant improvement for devs that use it in form validation.

We were trying to use zod for general validation (client/server) with superforms but the inability to bail early on the first error prevents this, as valuable server resources are wasted when zod still tries to validate the rest of the data even though the first entry is already faulty.

Devs overwhelmingly want to have the option for a early bail mode, I’m really wondering why the maintainers are so set against it. It’s just an option, and it’s always good to have options, right @colinhacks. Plus it could even gain on performance tests against new contenders like vinejs.

Plus. It would be a significant improvement for zod.

Definitely still relevant and would be greatly appreciated

+1

Bailing out on very first error, should be as simple as throwing an exception the moment you stumble on one. No?

There is support for aborting early when using .superRefine. It is more work but might fit your use case.

hey, @irrelevation thank you for pointing that out, but I think .superRefine avoids the main benefit of using Zod (or other type inference library), which is expressing types in a declarative way.

hey, @colinhacks, first of all, thank you for the great work 💪🏽! I understand that’s not the intended original use case for Zod. I’m moving from Joi – which I like, but lacks the type inference Zod does so well – and my idea was to migrate all schemas. My problem is that I’ve been also using Joi to validate HTTP payloads on a Hapi service and some of those validations can be expensive.

Maybe my case is too specific, and it wouldn’t worth the changes, but maybe HTTP payload validation could be a new use case that Zod might also address 😃 Otherwise, and obviously, please, close the issue – I don’t want to bother you with things out of the scope of the project.

@colinhacks what would be the problem supporting something such as abortEarly like Joi/Yup does?

Furthermore, what would be the downside of it? What about performance improvements?

schema.parse(value, { abortEarly: true })
schema.safeParse(value, { abortEarly: true })

await schema.parseAsync(value, { abortEarly: true })
await schema.safeParseAsync(value, { abortEarly: true })

I run a fetch to check if duplicates exists in database for example email or username. ( i have a lot of unique columns in other tables too) I want it to run fetch only if the basic frontend validations passed, otherwise its a useless server request. it would be poop DX if I have to replace all my validations with superRefine just to have abort early and improve performance … zod needs to have this essential feature, and if this use case is not common then I am accusing most developers of writing bad code

currently i am running my fetch separate from zod which is slightly inconvenient for the beautiful generic structure of my app

+1 I only display the first error of each field

+1 it would be a great option to have, it would avoid duplicate validations.

+1 guys. In my case, I have a refine that is an asyncronous call (to verify email addresses). It fires so many times even if an invalid email is typed. Great waste to my mind. I’m trying using a superrefiner, but doesn’t look like I can now that the field is already invalid… Should I move all the validations there? Let’s hope not!

Personally I like the bail operator suggested in another thread.

I guess that at least having the current validity state in the super refine would also work in my scenario!

Workaround in the meantime

const validation = z.email().min(2); // Just an example, you could have more
const refinedValidation = validation.refine(async (value) => {
    try {
        validation.parse(value);

        return await validateEmail(value).then(() => !0).catch(() => !1);
    }
    catch (e) {
        // Skipping as already invalid
        return !0;
    }
}, "error.marketing_cloud_invalid_email");

I think it’s awful considering that for multiple refiners is’t a lot of code, but this should work.

(I’ll update if not 😁)

hey, @TamirCode, @JaneJeon and @carlBgood, I had closed this issue because I believe this use case is not intended to be tackled by Zod (which I think is fair). I reopened because your messages indicate this may be an important subject to you. I opened this ticket some time ago, and I’ve already solved my problem, so I won’t really be an active part of this conversation. Cheers

I believe this is a case of dualing use-cases: people fought pretty hard for the current API of “surface as many errors as possible” since we used to fail fast.

A pretty common use-case is form-validation utilizing database queries. Example: if I want to validate a simple email/password form, I can string().email(). But if want to combine that with the database query to keep all my errors in sync and make it string().email().superRefine() I can only halt/abort after the first db query I run in the refinement - and if any previous validators in the chain failed - I’m running the db query for nothing - and either with potentially unsecure data or I need to re-validate in the refinement to ensure it’s good to go.

I think this very neatly describes why I avoid using the blessed internal validators: they’re just too “special case”-y and require to much refactoring if you stray outside of their narrow use-case. If you need to control the parallelism of your validation logic, I would building up some composable superRefine-style functions and sticking with those.

Another option is to use pipe to abort earlier on email and put your more expensive refinements in the schema you’re piping to. So:

const schema = z.object({
  foo: z
    .string()
    .email()
    .pipe(
      z.string().superRefine(() => {
        // super expensive calc
      })
    ),
});

That won’t work for all use-cases and now you have to deal with the ZodPipeline type instead of just ZodEffects, but it might work for some large percentage of your usage.