Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-5088-object-field-validation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"vee-validate": patch
---

Fix useField with object values not triggering validation (#5088)
9 changes: 9 additions & 0 deletions packages/vee-validate/src/useForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,13 @@ export function useForm<
delete extraErrorsBag.value[path];
}

// Also clean up extra errors for the expected path when it was hoisted to a different path
// This handles cases where schema reports errors at nested sub-paths (e.g., 'someObject.id')
// but the field is registered at a parent path (e.g., 'someObject')
if (pathState && expectedPath !== path && extraErrorsBag.value[expectedPath]) {
delete extraErrorsBag.value[expectedPath];
}
Comment on lines +389 to +394
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extraErrorsBag keys are stored using normalizeFormPath(), but this cleanup checks/deletes using the raw expectedPath. If the schema reports a path using dot-index notation (e.g. items.0.id) while extraErrorsBag stores items[0].id, the stale entry won’t be removed. Normalize expectedPath (and use it for the comparison) before reading/deleting from extraErrorsBag to ensure cleanup works for all path formats.

Copilot uses AI. Check for mistakes.

// field not rendered
if (!pathState) {
setFieldError(path, messages);
Expand Down Expand Up @@ -811,6 +818,8 @@ export function useForm<
});

opts?.force ? forceSetValues(newValues, false) : setValues(newValues, false);
// Clear extra errors that were set for paths without a pathState (e.g., nested paths of hoisted fields)
extraErrorsBag.value = {};
setErrors(resetState?.errors || {});
submitCount.value = resetState?.submitCount || 0;
nextTick(() => {
Expand Down
71 changes: 71 additions & 0 deletions packages/vee-validate/tests/useForm.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
} from '@/vee-validate';
import { mountWithHoc, setValue, flushPromises, dispatchEvent } from './helpers';
import * as z from 'zod';
import * as yup from 'yup';
import { onMounted, ref, Ref } from 'vue';
import { ModelComp, CustomModelComp } from './helpers/ModelComp';

Expand Down Expand Up @@ -1489,4 +1490,74 @@ describe('useForm()', () => {
form.setValues({ file: f2 });
expect(form.values.file).toEqual(f2);
});

// #5088 - extraErrorsBag entries for hoisted/nested paths should be cleaned up after validation
test('useField with object values validates correctly after resetForm with delayed field rendering', async () => {
let form!: FormContext<any>;
let fieldCtx!: FieldContext<any>;
const showField = ref(false);

mountWithHoc({
setup() {
form = useForm({
validationSchema: yup.object({
someObject: yup
.object({
id: yup.number().required(),
title: yup.string(),
})
.nullable(),
}),
});

return {
showField,
};
},
template: `
<div>
<Child v-if="showField" />
</div>
`,
components: {
Child: {
setup() {
const field = useField('someObject');
fieldCtx = field;
return { value: field.value };
},
template: '<span id="fieldValue">{{ value }}</span>',
},
},
});

await flushPromises();

// At this point, the initial silent validation has run on empty form values.
// Yup reports errors at nested paths like 'someObject.id' (not 'someObject').
// These errors go into extraErrorsBag since no pathStates exist yet.

// Reset the form with valid values and immediately show the field
form.resetForm({
values: {
someObject: {
id: 1,
title: 'Lorem Ipsum',
},
},
});
showField.value = true;
await flushPromises();

// The form values should be set correctly
expect(form.values.someObject).toEqual({ id: 1, title: 'Lorem Ipsum' });
// The field should have the correct value
expect(fieldCtx.value.value).toEqual({ id: 1, title: 'Lorem Ipsum' });
// The field meta should report valid
expect(fieldCtx.meta.valid).toBe(true);
// The form meta should report valid since the values satisfy the schema
expect(form.meta.value.valid).toBe(true);
// No errors should be reported
expect(form.errors.value).toEqual({});
});
});
Loading