Repository: logaretm/vee-validate
Pull request #5031 (2025-04-25 19:55:31)

fix: valibot variants not inferring required correctly
🔎 __Overview__

Similar to https://github.com/logaretm/vee-validate/pull/5014

I use the required field on useField#meta to automatically show an "Optional" text on an input. I found that having `v.variant` breaks this.



changeset-bot[bot]: ###  ⚠️  No Changeset found

Latest commit: 12616efebaa43b32e52032b834f510d21ec79b43

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. **If these changes should result in a version bump, you need to add a changeset.**

<details><summary>This PR includes no changesets</summary>

  When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

</details>

[Click here to learn what changesets are, and how to add one](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add a changeset to this PR](https://github.com/jack-allocate/vee-validate/new/fix/valibot-variants?filename=.changeset/hot-knives-listen.md&value=---%0A%22%40vee-validate%2Fvalibot%22%3A%20patch%0A---%0A%0Afix%3A%20valibot%20variants%20not%20inferring%20required%20correctly%0A)


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/logaretm/vee-validate/pull/5031?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abdelrahman+Awad) Report
Attention: Patch coverage is `94.11765%` with `1 line` in your changes missing coverage. Please review.
> Project coverage is 89.37%. Comparing base [(`9803aa2`)](https://app.codecov.io/gh/logaretm/vee-validate/commit/9803aa27cb1297026a1944f83f99da4b8bd2ee97?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abdelrahman+Awad) to head [(`12616ef`)](https://app.codecov.io/gh/logaretm/vee-validate/commit/12616efebaa43b32e52032b834f510d21ec79b43?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abdelrahman+Awad).

| [Files with missing lines](https://app.codecov.io/gh/logaretm/vee-validate/pull/5031?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abdelrahman+Awad) | Patch % | Lines |
|---|---|---|
| [packages/valibot/src/index.ts](https://app.codecov.io/gh/logaretm/vee-validate/pull/5031?src=pr&el=tree&filepath=packages%2Fvalibot%2Fsrc%2Findex.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abdelrahman+Awad#diff-cGFja2FnZXMvdmFsaWJvdC9zcmMvaW5kZXgudHM=) | 94.11% | [1 Missing :warning: ](https://app.codecov.io/gh/logaretm/vee-validate/pull/5031?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abdelrahman+Awad) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #5031      +/-   ##
==========================================
+ Coverage   89.36%   89.37%   +0.01%     
==========================================
  Files          93       93              
  Lines        8046     8063      +17     
  Branches     1394     1400       +6     
==========================================
+ Hits         7190     7206      +16     
- Misses        849      850       +1     
  Partials        7        7              
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/logaretm/vee-validate/pull/5031?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abdelrahman+Awad).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abdelrahman+Awad).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
netlify[bot]: ### <span aria-hidden="true">👷</span> Deploy Preview for *vee-validate-docs* processing.


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | 48162525aea47f2798eea5f47a9e3223a455eb24 |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/vee-validate-docs/deploys/6823982eb371b1000858aa61 |

Patch:
diff --git a/.changeset/angry-eels-confess.md b/.changeset/angry-eels-confess.md
new file mode 100644
index 00000000..827b3cb8
--- /dev/null
+++ b/.changeset/angry-eels-confess.md
@@ -0,0 +1,5 @@
+---
+"@vee-validate/valibot": patch
+---
+
+fix: valibot variants not inferring required correctly
diff --git a/packages/valibot/src/index.ts b/packages/valibot/src/index.ts
index 9abf3565..83df1124 100644
--- a/packages/valibot/src/index.ts
+++ b/packages/valibot/src/index.ts
@@ -27,6 +27,9 @@ import {
   Config,
   IntersectSchema,
   IntersectIssue,
+  VariantSchema,
+  VariantOptions,
+  VariantIssue,
 } from 'valibot';
 import { isIndex, isObject, merge, normalizeFormPath } from '../../shared';
 
@@ -140,6 +143,10 @@ function getSchemaForPath(
     return schema.options.map(o => getSchemaForPath(path, o)).find(Boolean) ?? null;
   }
 
+  if (isVariantSchema(schema)) {
+    return schema.options.map(o => getSchemaForPath(path, o)).find(Boolean) ?? null;
+  }
+
   if (!isObjectSchema(schema)) {
     return null;
   }
@@ -161,6 +168,10 @@ function getSchemaForPath(
       currentSchema = currentSchema.options.find(o => isObjectSchema(o) && o.entries[p]) ?? currentSchema;
     }
 
+    if (isVariantSchema(currentSchema)) {
+      currentSchema = currentSchema.options.find(o => isObjectSchema(o) && o.entries[p]) ?? currentSchema;
+    }
+
     if (isObjectSchema(currentSchema)) {
       currentSchema = currentSchema.entries[p] || null;
       continue;
@@ -208,3 +219,9 @@ function isIntersectSchema(
 > {
   return schema.type === 'intersect';
 }
+
+function isVariantSchema(
+  schema: BaseSchema<unknown, unknown, BaseIssue<unknown>> | BaseSchemaAsync<unknown, unknown, BaseIssue<unknown>>,
+): schema is VariantSchema<string, VariantOptions<string>, ErrorMessage<VariantIssue> | undefined> {
+  return schema.type === 'variant';
+}


Test patch:
diff --git a/packages/valibot/tests/valibot.spec.ts b/packages/valibot/tests/valibot.spec.ts
index 0c474feb..01bb9bef 100644
--- a/packages/valibot/tests/valibot.spec.ts
+++ b/packages/valibot/tests/valibot.spec.ts
@@ -656,6 +656,184 @@ test('reports required state for schema intersections with nested fields', async
   );
 });
 
+test('reports required state for variant schemas', async () => {
+  const metaSpy = vi.fn();
+  mountWithHoc({
+    setup() {
+      const ScheduledDateSchema = v.object({
+        dateType: v.literal('schedule'),
+        date: v.pipe(v.string(), v.isoDate()),
+      });
+
+      const ImmediateDateSchema = v.object({
+        dateType: v.literal('immediate'),
+      });
+
+      const PaymentDetailsFormSchema = v.variant('dateType', [ImmediateDateSchema, ScheduledDateSchema]);
+
+      const schema = toTypedSchema(PaymentDetailsFormSchema);
+
+      useForm({
+        validationSchema: schema,
+      });
+
+      const { meta: dateType } = useField('dateType');
+      const { meta: date } = useField('date');
+
+      metaSpy({
+        dateType: dateType.required,
+        date: date.required,
+      });
+
+      return {
+        schema,
+      };
+    },
+    template: `<div></div>`,
+  });
+
+  await flushPromises();
+  await expect(metaSpy).toHaveBeenLastCalledWith(
+    expect.objectContaining({
+      dateType: true,
+      date: true,
+    }),
+  );
+});
+
+test('reports required state for variant schemas with nested fields', async () => {
+  const metaSpy = vi.fn();
+  mountWithHoc({
+    setup() {
+      const ComplexVariantSchema = v.variant('kind', [
+        v.variant('type', [
+          v.object({
+            kind: v.literal('fruit'),
+            type: v.literal('apple'),
+            item: v.object({
+              name: v.string(),
+              price: v.number(),
+            }),
+          }),
+          v.object({
+            kind: v.literal('fruit'),
+            type: v.literal('banana'),
+            item: v.object({
+              name: v.string(),
+              price: v.number(),
+            }),
+          }),
+        ]),
+        v.variant('type', [
+          v.object({
+            kind: v.literal('vegetable'),
+            type: v.literal('carrot'),
+            item: v.object({
+              name: v.string(),
+              price: v.number(),
+            }),
+          }),
+          v.object({
+            kind: v.literal('vegetable'),
+            type: v.literal('tomato'),
+            item: v.object({
+              name: v.string(),
+              price: v.number(),
+            }),
+          }),
+        ]),
+      ]);
+
+      const schema = toTypedSchema(ComplexVariantSchema);
+
+      useForm({
+        validationSchema: schema,
+      });
+
+      const { meta: kind } = useField('kind');
+      const { meta: type } = useField('type');
+      const { meta: item } = useField('item');
+
+      metaSpy({
+        kind: kind.required,
+        type: type.required,
+        item: item.required,
+      });
+
+      return {
+        schema,
+      };
+    },
+    template: `<div></div>`,
+  });
+
+  await flushPromises();
+  await expect(metaSpy).toHaveBeenLastCalledWith(
+    expect.objectContaining({
+      kind: true,
+      type: true,
+      item: true,
+    }),
+  );
+});
+
+test('reports required state for variant schemas when combined with intersections', async () => {
+  const metaSpy = vi.fn();
+  mountWithHoc({
+    setup() {
+      const ScheduledDateSchema = v.object({
+        dateType: v.literal('schedule'),
+        date: v.pipe(v.string(), v.isoDate()),
+      });
+
+      const ImmediateDateSchema = v.object({
+        dateType: v.literal('immediate'),
+      });
+
+      const PaymentDetailsFormSchema = v.intersect([
+        v.variant('dateType', [ImmediateDateSchema, ScheduledDateSchema]),
+        v.object({
+          amount: v.pipe(v.number(), v.minValue(1)),
+          note: v.optional(v.string()),
+        }),
+      ]);
+
+      const schema = toTypedSchema(PaymentDetailsFormSchema);
+
+      useForm({
+        validationSchema: schema,
+      });
+
+      const { meta: dateType } = useField('dateType');
+      const { meta: date } = useField('date');
+      const { meta: amount } = useField('amount');
+      const { meta: note } = useField('note');
+
+      metaSpy({
+        dateType: dateType.required,
+        date: date.required,
+        amount: amount.required,
+        note: note.required,
+      });
+
+      return {
+        schema,
+      };
+    },
+    template: `<div></div>`,
+  });
+
+  await flushPromises();
+  await expect(metaSpy).toHaveBeenLastCalledWith(
+    expect.objectContaining({
+      dateType: true,
+      date: true,
+      amount: true,
+      note: false,
+    }),
+  );
+});
+
 test('allows passing valibot config', async () => {
   let errors!: Ref<string[]>;
   const wrapper = mountWithHoc({