Skip to content

Commit 9867e59

Browse files
fix: valibot variants not inferring required correctly (#5031)
* fix: valibot variants not inferring required correctly * chore: add changeset
1 parent 9803aa2 commit 9867e59

File tree

3 files changed

+200
-0
lines changed

3 files changed

+200
-0
lines changed

.changeset/angry-eels-confess.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@vee-validate/valibot": patch
3+
---
4+
5+
fix: valibot variants not inferring required correctly

packages/valibot/src/index.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ import {
2727
Config,
2828
IntersectSchema,
2929
IntersectIssue,
30+
VariantSchema,
31+
VariantOptions,
32+
VariantIssue,
3033
} from 'valibot';
3134
import { isIndex, isObject, merge, normalizeFormPath } from '../../shared';
3235

@@ -140,6 +143,10 @@ function getSchemaForPath(
140143
return schema.options.map(o => getSchemaForPath(path, o)).find(Boolean) ?? null;
141144
}
142145

146+
if (isVariantSchema(schema)) {
147+
return schema.options.map(o => getSchemaForPath(path, o)).find(Boolean) ?? null;
148+
}
149+
143150
if (!isObjectSchema(schema)) {
144151
return null;
145152
}
@@ -161,6 +168,10 @@ function getSchemaForPath(
161168
currentSchema = currentSchema.options.find(o => isObjectSchema(o) && o.entries[p]) ?? currentSchema;
162169
}
163170

171+
if (isVariantSchema(currentSchema)) {
172+
currentSchema = currentSchema.options.find(o => isObjectSchema(o) && o.entries[p]) ?? currentSchema;
173+
}
174+
164175
if (isObjectSchema(currentSchema)) {
165176
currentSchema = currentSchema.entries[p] || null;
166177
continue;
@@ -208,3 +219,9 @@ function isIntersectSchema(
208219
> {
209220
return schema.type === 'intersect';
210221
}
222+
223+
function isVariantSchema(
224+
schema: BaseSchema<unknown, unknown, BaseIssue<unknown>> | BaseSchemaAsync<unknown, unknown, BaseIssue<unknown>>,
225+
): schema is VariantSchema<string, VariantOptions<string>, ErrorMessage<VariantIssue> | undefined> {
226+
return schema.type === 'variant';
227+
}

packages/valibot/tests/valibot.spec.ts

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -656,6 +656,184 @@ test('reports required state for schema intersections with nested fields', async
656656
);
657657
});
658658

659+
test('reports required state for variant schemas', async () => {
660+
const metaSpy = vi.fn();
661+
mountWithHoc({
662+
setup() {
663+
const ScheduledDateSchema = v.object({
664+
dateType: v.literal('schedule'),
665+
date: v.pipe(v.string(), v.isoDate()),
666+
});
667+
668+
const ImmediateDateSchema = v.object({
669+
dateType: v.literal('immediate'),
670+
});
671+
672+
const PaymentDetailsFormSchema = v.variant('dateType', [ImmediateDateSchema, ScheduledDateSchema]);
673+
674+
const schema = toTypedSchema(PaymentDetailsFormSchema);
675+
676+
useForm({
677+
validationSchema: schema,
678+
});
679+
680+
const { meta: dateType } = useField('dateType');
681+
const { meta: date } = useField('date');
682+
683+
metaSpy({
684+
dateType: dateType.required,
685+
date: date.required,
686+
});
687+
688+
return {
689+
schema,
690+
};
691+
},
692+
template: `<div></div>`,
693+
});
694+
695+
await flushPromises();
696+
await expect(metaSpy).toHaveBeenLastCalledWith(
697+
expect.objectContaining({
698+
dateType: true,
699+
date: true,
700+
}),
701+
);
702+
});
703+
704+
test('reports required state for variant schemas with nested fields', async () => {
705+
const metaSpy = vi.fn();
706+
mountWithHoc({
707+
setup() {
708+
const ComplexVariantSchema = v.variant('kind', [
709+
v.variant('type', [
710+
v.object({
711+
kind: v.literal('fruit'),
712+
type: v.literal('apple'),
713+
item: v.object({
714+
name: v.string(),
715+
price: v.number(),
716+
}),
717+
}),
718+
v.object({
719+
kind: v.literal('fruit'),
720+
type: v.literal('banana'),
721+
item: v.object({
722+
name: v.string(),
723+
price: v.number(),
724+
}),
725+
}),
726+
]),
727+
v.variant('type', [
728+
v.object({
729+
kind: v.literal('vegetable'),
730+
type: v.literal('carrot'),
731+
item: v.object({
732+
name: v.string(),
733+
price: v.number(),
734+
}),
735+
}),
736+
v.object({
737+
kind: v.literal('vegetable'),
738+
type: v.literal('tomato'),
739+
item: v.object({
740+
name: v.string(),
741+
price: v.number(),
742+
}),
743+
}),
744+
]),
745+
]);
746+
747+
const schema = toTypedSchema(ComplexVariantSchema);
748+
749+
useForm({
750+
validationSchema: schema,
751+
});
752+
753+
const { meta: kind } = useField('kind');
754+
const { meta: type } = useField('type');
755+
const { meta: item } = useField('item');
756+
757+
metaSpy({
758+
kind: kind.required,
759+
type: type.required,
760+
item: item.required,
761+
});
762+
763+
return {
764+
schema,
765+
};
766+
},
767+
template: `<div></div>`,
768+
});
769+
770+
await flushPromises();
771+
await expect(metaSpy).toHaveBeenLastCalledWith(
772+
expect.objectContaining({
773+
kind: true,
774+
type: true,
775+
item: true,
776+
}),
777+
);
778+
});
779+
780+
test('reports required state for variant schemas when combined with intersections', async () => {
781+
const metaSpy = vi.fn();
782+
mountWithHoc({
783+
setup() {
784+
const ScheduledDateSchema = v.object({
785+
dateType: v.literal('schedule'),
786+
date: v.pipe(v.string(), v.isoDate()),
787+
});
788+
789+
const ImmediateDateSchema = v.object({
790+
dateType: v.literal('immediate'),
791+
});
792+
793+
const PaymentDetailsFormSchema = v.intersect([
794+
v.variant('dateType', [ImmediateDateSchema, ScheduledDateSchema]),
795+
v.object({
796+
amount: v.pipe(v.number(), v.minValue(1)),
797+
note: v.optional(v.string()),
798+
}),
799+
]);
800+
801+
const schema = toTypedSchema(PaymentDetailsFormSchema);
802+
803+
useForm({
804+
validationSchema: schema,
805+
});
806+
807+
const { meta: dateType } = useField('dateType');
808+
const { meta: date } = useField('date');
809+
const { meta: amount } = useField('amount');
810+
const { meta: note } = useField('note');
811+
812+
metaSpy({
813+
dateType: dateType.required,
814+
date: date.required,
815+
amount: amount.required,
816+
note: note.required,
817+
});
818+
819+
return {
820+
schema,
821+
};
822+
},
823+
template: `<div></div>`,
824+
});
825+
826+
await flushPromises();
827+
await expect(metaSpy).toHaveBeenLastCalledWith(
828+
expect.objectContaining({
829+
dateType: true,
830+
date: true,
831+
amount: true,
832+
note: false,
833+
}),
834+
);
835+
});
836+
659837
test('allows passing valibot config', async () => {
660838
let errors!: Ref<string[]>;
661839
const wrapper = mountWithHoc({

0 commit comments

Comments
 (0)