colinhacks / zod

TypeScript-first schema validation with static type inference
https://zod.dev
MIT License
32.97k stars 1.14k forks source link

[question] is it possible to stop parsing on the first error? #1403

Open gerardolima opened 2 years ago

gerardolima commented 2 years ago

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?

colinhacks commented 2 years ago

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.

gerardolima commented 2 years ago

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.

jayarjo commented 1 year ago

+1

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

srieas commented 1 year ago

+1

irrelevation commented 1 year ago

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

gerardolima commented 1 year ago

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.

carlBgood commented 1 year ago

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!

stale[bot] commented 1 year ago

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.

JaneJeon commented 1 year ago

Definitely still relevant and would be greatly appreciated

scotttrinh commented 1 year ago

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.

valentsea commented 1 year ago

Plus. It would be a significant improvement for zod.

TamirCode commented 1 year ago

+1 I only display the first error of each field

TamirCode commented 1 year ago

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

bulicmatko commented 1 year ago

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:

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.

TamirCode commented 1 year ago

@gerardolima Y u close 🫤

JaneJeon commented 1 year ago

Was this added in zod, or just closed because the original poster no longer needs it anymore?

carlBgood commented 1 year ago

Closed?!?

gerardolima commented 1 year ago

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

OctavioSerpe commented 1 year ago

I have stumbled into the same issue, I need to bail validation on specific errors (like express-validator allows to https://express-validator.github.io/docs/api/validation-chain/#bail). One approach could be to do chain validations separately (tedious) or maybe just get the first error and do the heavy calculation on another schema.

I believe it would be a nice addition to this incredible tool.

Cheers

rafaell-lycan commented 1 year ago

@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 })
gustawdaniel commented 11 months ago

It is implemnted in valibot https://github.com/fabian-hiller/valibot/issues/18

commit: https://github.com/fabian-hiller/valibot/commit/37ac397666e2c8bc2529d98f50d861026fbacc93

interface:

abortPipeEarly?: boolean
KikoCosmetics commented 11 months ago

+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 😁)

jyotirmaybarman commented 10 months ago

+1 from my side

catinrage commented 9 months ago

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

MentalGear commented 6 months ago

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.

gregmsanderson commented 6 months ago

@MentalGear I've just been finding exactly the same issue as you. Very frustrating.

I notice that another library superforms supports is VineJS which does appear to have a "bail" mode https://vinejs.dev/docs/schema_101#bail-mode

However that currently throws a "node:dns" warning. Nothing is ever simple!

Update: Ah, I see that it's intended only for backend https://vinejs.dev/docs/introduction#can-i-use-vinejs-in-a-front-end-application So if your superforms usage is part of a Sveltekit server.js file, in theory that should work and solve the problem. But not for an SPA +page.svelte. The search continues.

abpbackup commented 5 months ago

abortEarly is very handy when you have multi-step forms. Especially, because the superRefine error order is messy so you end up showing errors some steps ahead affecting the using experience.

jeslenbucci commented 2 months ago

Like many here, I'm also queyring my database to check for email addresses in my validation and noticed that it was executing every time, even when the email was not valid. To get around this, I just added a bit of a mix from what the others mentioned.

Rather than chaining all the default checks before the superRefine(), I just do within it. Then use the fatal flag when using ctx.addIssue().

const emailSchema = z.string().superRefine(async (email, ctx) => {
  const emailTest = z.string().email().safeParse(email)

    if (!emailTest.success) {
      ctx.addIssue({
        code: ZodIssueCode.invalid_string,
        validation: 'email',
        message: 'Invalid Email',
        fatal: true,
      })
      return z.NEVER
    }

    // Do email query here
})
jeslenbucci commented 2 months ago

Here's an even more complex example that I have working with react-hook-forms with a dynamic form builder I created. What I do is first create the basic schema as a constant. Then, in the super refinement, I do a safe parse. If it's not successful, I loop over each issue and spread the error into the ctx.addIssue() method. Then I return z.NEVER. This recreates the same basic validation that you'd get without the refinement.

In my example, I'm parsing a large address object that allows for both mailing and physical addresses. I add an additional uuid prop to my object because that's the way it's set up in my forms. Otherwise, you wouldn't need to shape your object as I do with my uuidSchema

  // define the basic schema
  const basicAddressSchema = z.object({
    mailing: z.object({
      line1: z.string().min(2),
      line2: z.string().optional(),
      city: z.string().min(2),
      subdivision: z.string().min(2),
      postalCode: z.string().min(2),
      country: z.string().min(2),
    }),
    physical: z.object({
      isSameAsMailing: z.boolean(),
      line1: z.string().min(2),
      line2: z.string().optional(),
      city: z.string().min(2),
      subdivision: z.string().min(2),
      postalCode: z.string().min(2),
      country: z.string().min(2),
    }),
  })

  // set up actual address schmea with the super refine method
  const addressSchema = basicAddressSchema.superRefine((address, ctx) => {
    // create my uuid schema, you would need to match your actual data
    const uuidSchema = z.object({
      [uuid]: z.object({
        address: basicAddressSchema,
      }),
    })

    // parse the schema
    const uuidValidation = uuidSchema.safeParse({ [uuid]: { ...address } })

    // if there are basic errosrs
    if (!uuidValidation.success) {
      // iterate over each
      uuidValidation.error.issues.forEach((error) => {
        // add them to the ctx
        ctx.addIssue({
          ...error,
        })
      })
      // return to prevent any further cod execution
      return z.NEVER
    }

    console.log("this will not appear if there are errors")
  })
rhaeyx commented 1 month ago

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.

+1 to this, I have the same use case with this reply. Has this been added or are there no plans in adding this?

sebastien-comeau commented 1 month ago

+1

buzzy commented 1 month ago

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.

Not a common use-case? That is one of the weirdest things I ever read! I would argue the opposite. Why would you WANT to keep validating a field that already failed validation? If a field is set to "required", why would you need to also check if it's a valid e-mail if it's blank? Makes absolutely no sense. Abort early should be the DEFAULT. What exactly would you do with all those validations that fails for an empty string? Show 5 failed validation messages to the user? Crazy.

dmgauthier commented 1 month ago

+1

buzzy commented 1 month ago

+1

abdurahmanshiine commented 4 weeks ago

+1

AlexanderBich commented 3 weeks ago

+1

offizium-berndstorath commented 3 weeks ago

@colinhacks Please reconsider this

yousufiqbal commented 1 week ago

Sakht londa he. Kr de bhai implement.