react-hook-form / resolvers

📋 Validation resolvers: Yup, Zod, Superstruct, Joi, Vest, Class Validator, io-ts, Nope, computed-types, typanion, Ajv, TypeBox, ArkType, Valibot, effect-ts and VineJS
https://react-hook-form.com/
MIT License
1.72k stars 156 forks source link

zod's superRefine not working properly #538

Open rlesniak opened 1 year ago

rlesniak commented 1 year ago

Describe the bug I want to use zod's superRefine validation function. hook form shows that form is invalid, but errors object is empty

To Reproduce Steps to reproduce the behavior:

  1. Go to https://stackblitz.com/edit/vitejs-vite-dwvjn3?file=src/App.jsx
  2. See that isValid is false but errors object is empty

Codesandbox link (Required) https://stackblitz.com/edit/vitejs-vite-dwvjn3?file=src/App.jsx

Expected behavior error object should have Block is too short error message

Desktop (please complete the following information):

jorisre commented 1 year ago

I doubt that the problem is linked to Zod's superRefine since the same issue arises with other resolvers like Yup, as I have tested. Although formState.isValid is true without a resolver, it is initially false when used with a resolver.

@bluebill1049 , could it be possible that there is something related to the usage of resolvers on the react-hook-form's end?

rlesniak commented 1 year ago

Its for sure not zods fault because its error object, either for common validation like nonempty() or superRefine is consistent image

bluebill1049 commented 1 year ago

Have you tried debug with the following:

resolver: async (data, context, options) => {
  // you can debug your validation schema here
  console.log('formData', data)
  console.log('validation result', await anyResolver(schema)(data, context, options))
  return anyResolver(schema)(data, context, options)
},
jorisre commented 1 year ago

@bluebill1049 I did it as I said in my previous reply

GuhPires commented 1 year ago

Having the same issue when using the refine method as well. Debugged as @bluebill1049 had shown and the output is exactly as expected by the resolver: an object with values and errors (with the error "thrown" by the refine function populated in that object).

The isValid prop is updated accordingly but the errors object is still empty

ms10596 commented 1 year ago

I'm facing the same issue using superRefine.

emielvanseveren commented 1 year ago

Having the same issue as @GuhPires.

nag-murali commented 1 year ago

Facing the same issues with superRefine

jorisre commented 1 year ago

@bluebill1049 Any thoughts?

bluebill1049 commented 1 year ago

@bluebill1049 Any thoughts?

I will take a look at it 👍

nag-murali commented 1 year ago

Describe the bug using zod's superRefine validation function. errors object is empty. Its working fine in one order not the other way. ( check this video) Link: (Screen Recording 2023-08-21 at 2.49.16 PM.zip) Error Video Link: (https://github.com/react-hook-form/resolvers/assets/93373775/82cebe8b-228b-4a7b-a4fd-980cf97310bb)

To Reproduce Steps to reproduce the behavior:

Go to https://codesandbox.io/p/sandbox/elated-mendeleev-v92vvs See that errors object is empty, even though validation is correct.

Expected behavior errors object of the react form should have the errors of the respective fields.

Desktop (please complete the following information):

OS: macOS Ventura 13 Browser Chrome Version 116

Please let me know if the way of implementation is wrong !!

stramel commented 1 year ago

@bluebill1049 I noticed when trying to use the .refine and .superRefine it would work depending on placement. This was due to the fact that the ref wouldn't necessarily be populated if there wasn't something registered for the path I was validating.

It would be nice if it could follow how setError works, and allow paths to register errors

bluebill1049 commented 1 year ago

hey @stramel 👋 I think the whole scheme validation is based on registered input, not sure if it's a straightforward implementation switch and setError was a single path update which is a lot easier.

stramel commented 1 year ago

Hey @bluebill1049 👋🏼

I'm currently using the Zod schema validation pretty easily but there are a couple of cases that aren't working well for me.

I have:

The setError was more referring to how it can apply an error to a non-registered input.

bluebill1049 commented 1 year ago

validating the schema, the errors aren't being cleared after applying to a registered input (probably a separate issue)

A separate issue, hook form is field level based for validation and consistent validation strategy. I don't have a solution in my head without re-render the entre form on each form state.

stramel commented 1 year ago

validating the schema, the errors aren't being cleared after applying to a registered input (probably a separate issue)

A separate issue, hook form is field level based for validation and consistent validation strategy. I don't have a solution in my head without re-render the entre form on each form state.

That's fair, I will come up with a workaround for that.

I will note though, that the isValid property still shows invalid even though there are no errors since Zod reports errors but they don't exist within the fields. I would expect that the errors object would have something if the isValid property is reporting invalid.

camin-mccluskey commented 1 year ago

Also facing this issue isValid is false with empty errors object. Does anyone know if manually setting the error path in refine results in an error object? (I don't think this would be a solution in my case however, as the path is dynamic as part of an array input).

bu3alwa commented 11 months ago

This only happens on first load but if you have a form and you submit or have it re-validate on change it will give you the error object.

simpros commented 9 months ago

Any update on this?

douglasgomes98 commented 6 months ago

Any update on this?

FoiDot commented 5 months ago

Hi! Any update on this issue? I'm manually triggering the validation if isValid = false and all the inputs are filled. But it will be nice to have a native solution.

rinqtmith commented 5 months ago

I had same issue and I have tried adding proper path to addIssue and now it works. Before adding path, error was there without a path.

  .superRefine((data, ctx) => {
    if (data.table === "linear" && data.dimension < 0.5) {
      ctx.addIssue({
        code: z.ZodIssueCode.too_small,
        minimum: 0.5,
        type: "string",
        inclusive: true,
        message: "Value cannot be smaller than 0.5.",
        path: ["dimension"],
      });
    }}
stefan-girlich commented 5 months ago

Hi! Any update on this issue? I'm manually triggering the validation if isValid = false and all the inputs are filled. But it will be nice to have a native solution.

@FoiDot Would you mind sharing the code for your solution? I tried the following, but errors is still empty:

useEffect(() => {
    if (!isValid) trigger('batches')
  }, [isValid])
FoiDot commented 5 months ago

Hi! Any update on this issue? I'm manually triggering the validation if isValid = false and all the inputs are filled. But it will be nice to have a native solution.

@FoiDot Would you mind sharing the code for your solution? I tried the following, but errors is still empty:

useEffect(() => {
    if (!isValid) trigger('batches')
  }, [isValid])

@stefan-girlich I did something like this:

useEffect(() => {
        // I have some inputs working with Controller and setState.
        // That means mode = 'all' | 'onChange' | etc doesn't work
        // So I need to clean the errors manually with the clearErrors from useForm.
        if (isValid) clearErrors()
       // Check if all your inputs are filled
        else if (
            !isValid &&
            watch('bank') &&
            watch('accountType') &&
            watch('accountNumber')
        )
            trigger()
 }, [isValid])

Make sure to have your path in your schema

if (accountNumber.startsWith('10') && bank === '01')
       ctx.addIssue({
       code: z.ZodIssueCode.custom,
       path: ['accountNumber'],
       message: `Your Account number cannot start with 10`,
       fatal: true,
 })

return z.NEVER
MusaGillani commented 5 months ago

following

stefan-girlich commented 5 months ago

This fix worked for me. tldr:

- path: [...ctx.path, idx, 'myProperty'],
+ path: [`[${idx}].myProperty`],

Kudos @bluebill1049 for the debug code; I wouldn't have found this issue without it. 💯

GPetrites commented 5 months ago

I'm running into the same issue. In my case the superRefine is needing to flag an error on a field which has not been edited. This field is only required if another field has a given value. When this happens, RHF is recognizing that the form is invalid but doesn't add the error to the field or form.

I can get around this, mostly, by using the trigger function on the form to tell RHF to validate the fields that were never edited.

You can see this in the Codesandbox link in the original error report. When you run the sandbox as-is, the form is invalid with no errors. But if you add the following at line 24 then the error message appears on the form:

form.trigger("start")

This is not quite optimal as I need to remember to call trigger to force RHF to fully recognize the validation error.

Bottom line, if superRefine is flagging an error on a field which is not dirty (not touched?), somehow add a call to trigger tied to an event on the form which tells the form to check if the field is valid. For example:

    useEffect(() => {
        trigger(["fieldReceivingError"]);
    }, [fieldCausingError.field.value]);
andredewaard commented 1 month ago

Same issue here, I have an object where you need to fill in a start and end time, i have a refine on the object that checks if the start_time is before the end_time, i can throw the error on the start_time like this.

export const CreateWorkActivityRequest = object({
  start_time: date(),
  end_time: date(),
}).refine(
  (val) => {
    if (val.type !== 'travel') {
      return !isAfter(val.start_time, val.end_time)
    }
    return true
  },
  {
    message: 'Start tijd is later dan eind tijd',
    path: ['start_time']
  }
)

But i want to make use of the 'root' property in the errors so i can show the error at the end of the form like we do with server errors

setError("root.serverError", {
  type: "400",
})

but throwing this error will make the form pass but the object its sending is empty

.refine(
  (val) => {
    if (val.type !== 'travel') {
      return !isAfter(val.start_time, val.end_time)
    }
    return true
  },
  {
    message: 'Start tijd is later dan eind tijd',
    path: ['root', 'start_end_time_mismatch']
  }
)

with superRefine this also works

.superRefine(
  (val, ctx) => {
    if (val.type !== 'travel' && isAfter(val.start_time, val.end_time)) {
      console.log('validation failed')
      ctx.addIssue({
        code: ZodIssueCode.invalid_date,
        message: 'Start tijd is later dan eind tijd',
        path: ['start_time'],
      });
      return NEVER
    }
  },
)

but this doenst

.superRefine(
  (val, ctx) => {
    if (val.type !== 'travel' && isAfter(val.start_time, val.end_time)) {
      console.log('validation failed')
      ctx.addIssue({
        code: ZodIssueCode.invalid_date,
        message: 'Start tijd is later dan eind tijd',
        path: ['root', 'start_end_time_mismatch'],
      });
      return NEVER
    }
  },
)