From e88507832aff3ff617bfd76d60c24fb5ce6d3605 Mon Sep 17 00:00:00 2001 From: JIHOON LEE Date: Tue, 12 Aug 2025 13:13:13 +0900 Subject: [PATCH 1/5] fix(form-core): handle string array indices in prefixSchemaToErrors - Fix array path handling for Standard Schema validators - Support both numeric (Zod) and string (Yup) array indices Fixes #1683 --- .../form-core/src/standardSchemaValidator.ts | 6 +- .../tests/standardSchemaValidator.spec.ts | 157 ++++++++++++++++++ 2 files changed, 162 insertions(+), 1 deletion(-) diff --git a/packages/form-core/src/standardSchemaValidator.ts b/packages/form-core/src/standardSchemaValidator.ts index c0c5bde69..e556fe869 100644 --- a/packages/form-core/src/standardSchemaValidator.ts +++ b/packages/form-core/src/standardSchemaValidator.ts @@ -27,7 +27,11 @@ function prefixSchemaToErrors(issues: readonly StandardSchemaV1Issue[]) { .map((segment) => { const normalizedSegment = typeof segment === 'object' ? segment.key : segment - return typeof normalizedSegment === 'number' + const isArrayIndex = + typeof normalizedSegment === 'number' || + (typeof normalizedSegment === 'string' && /^\d+$/.test(normalizedSegment)) + + return isArrayIndex ? `[${normalizedSegment}]` : normalizedSegment }) diff --git a/packages/form-core/tests/standardSchemaValidator.spec.ts b/packages/form-core/tests/standardSchemaValidator.spec.ts index 2a03c1968..501daf82f 100644 --- a/packages/form-core/tests/standardSchemaValidator.spec.ts +++ b/packages/form-core/tests/standardSchemaValidator.spec.ts @@ -398,4 +398,161 @@ describe('standard schema validator', () => { it.todo( 'Should allow for `disableErrorFlat` to disable flattening `errors` array', ) + + describe('array path handling', () => { + it('should handle numeric array indices correctly', async () => { + const form = new FormApi({ + defaultValues: { + people: [{ name: '' }], + }, + validators: { + onChange: z.object({ + people: z.array( + z.object({ + name: z.string().min(1, 'Name is required'), + }), + ), + }), + }, + }) + + const field = new FieldApi({ + form, + name: 'people[0].name', + }) + + field.mount() + + field.setValue('') + expect(form.state.errors).toMatchObject([ + { + 'people[0].name': [{ message: 'Name is required' }], + }, + ]) + }) + + it('should handle string array indices correctly (Yup compatibility)', async () => { + // Mock a Standard Schema validator that returns string indices like Yup + const mockYupLikeValidator = { + '~standard': { + version: 1 as const, + vendor: 'mock-yup', + validate: (value: unknown) => { + const typedValue = value as { people?: { name?: string }[] } + if (!typedValue.people?.[0]?.name) { + return { + issues: [ + { + message: 'Name is required', + path: ['people', '0', 'name'], // String index like Yup + }, + ], + } + } + return { value: typedValue } + }, + }, + } + + const form = new FormApi({ + defaultValues: { + people: [{ name: '' }], + }, + validators: { + onChange: mockYupLikeValidator, + }, + }) + + const field = new FieldApi({ + form, + name: 'people[0].name', + }) + + field.mount() + + field.setValue('') + expect(form.state.errors).toMatchObject([ + { + 'people[0].name': [{ message: 'Name is required' }], + }, + ]) + }) + + it('should handle nested arrays with mixed numeric and string indices', async () => { + const form = new FormApi({ + defaultValues: { + users: [ + { + addresses: [ + { street: 'Main St' }, + { street: '' }, // This will fail validation + ], + }, + ], + }, + validators: { + onChange: z.object({ + users: z.array( + z.object({ + addresses: z.array( + z.object({ + street: z.string().min(1, 'Street is required'), + }), + ), + }), + ), + }), + }, + }) + + const field = new FieldApi({ + form, + name: 'users[0].addresses[1].street', + }) + + field.mount() + field.setValue('') + + expect(form.state.errors).toMatchObject([ + { + 'users[0].addresses[1].street': [{ message: 'Street is required' }], + }, + ]) + }) + + it('should handle regular object paths without array indices', async () => { + const form = new FormApi({ + defaultValues: { + user: { + profile: { + name: '', + }, + }, + }, + validators: { + onChange: z.object({ + user: z.object({ + profile: z.object({ + name: z.string().min(1, 'Name is required'), + }), + }), + }), + }, + }) + + const field = new FieldApi({ + form, + name: 'user.profile.name', + }) + + field.mount() + + field.setValue('') + expect(form.state.errors).toMatchObject([ + { + 'user.profile.name': [{ message: 'Name is required' }], + }, + ]) + }) + }) }) From b3f6cd1f7770dace3493ff9a3666b6f3764d067f Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 12 Aug 2025 08:39:35 +0000 Subject: [PATCH 2/5] ci: apply automated fixes and generate docs --- packages/form-core/src/standardSchemaValidator.ts | 11 +++++------ .../form-core/tests/standardSchemaValidator.spec.ts | 2 +- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/form-core/src/standardSchemaValidator.ts b/packages/form-core/src/standardSchemaValidator.ts index e556fe869..7948c799f 100644 --- a/packages/form-core/src/standardSchemaValidator.ts +++ b/packages/form-core/src/standardSchemaValidator.ts @@ -27,13 +27,12 @@ function prefixSchemaToErrors(issues: readonly StandardSchemaV1Issue[]) { .map((segment) => { const normalizedSegment = typeof segment === 'object' ? segment.key : segment - const isArrayIndex = + const isArrayIndex = typeof normalizedSegment === 'number' || - (typeof normalizedSegment === 'string' && /^\d+$/.test(normalizedSegment)) - - return isArrayIndex - ? `[${normalizedSegment}]` - : normalizedSegment + (typeof normalizedSegment === 'string' && + /^\d+$/.test(normalizedSegment)) + + return isArrayIndex ? `[${normalizedSegment}]` : normalizedSegment }) .join('.') .replace(/\.\[/g, '[') diff --git a/packages/form-core/tests/standardSchemaValidator.spec.ts b/packages/form-core/tests/standardSchemaValidator.spec.ts index 501daf82f..2894b20fb 100644 --- a/packages/form-core/tests/standardSchemaValidator.spec.ts +++ b/packages/form-core/tests/standardSchemaValidator.spec.ts @@ -512,7 +512,7 @@ describe('standard schema validator', () => { field.mount() field.setValue('') - + expect(form.state.errors).toMatchObject([ { 'users[0].addresses[1].street': [{ message: 'Street is required' }], From 86ff708238cb34f88ec4bf0ff2ffa91c03aa048e Mon Sep 17 00:00:00 2001 From: JIHOON LEE Date: Wed, 13 Aug 2025 01:21:53 +0900 Subject: [PATCH 3/5] fix(form-core): improve array index path handling in standard schema validator by inspecting form values --- .../form-core/src/standardSchemaValidator.ts | 57 ++++++++++----- .../tests/standardSchemaValidator.spec.ts | 70 +++++++++++++------ 2 files changed, 85 insertions(+), 42 deletions(-) diff --git a/packages/form-core/src/standardSchemaValidator.ts b/packages/form-core/src/standardSchemaValidator.ts index e556fe869..fb953e4d2 100644 --- a/packages/form-core/src/standardSchemaValidator.ts +++ b/packages/form-core/src/standardSchemaValidator.ts @@ -19,25 +19,43 @@ export type TStandardSchemaValidatorIssue< ? StandardSchemaV1Issue[] : never -function prefixSchemaToErrors(issues: readonly StandardSchemaV1Issue[]) { +function prefixSchemaToErrors( + issues: readonly StandardSchemaV1Issue[], + formValue: unknown, +) { const schema = new Map() for (const issue of issues) { - const path = [...(issue.path ?? [])] - .map((segment) => { - const normalizedSegment = - typeof segment === 'object' ? segment.key : segment - const isArrayIndex = - typeof normalizedSegment === 'number' || - (typeof normalizedSegment === 'string' && /^\d+$/.test(normalizedSegment)) - - return isArrayIndex - ? `[${normalizedSegment}]` - : normalizedSegment - }) - .join('.') - .replace(/\.\[/g, '[') - + const issuePath = issue.path ?? [] + + let currentFormValue = formValue + let path = '' + + for (let i = 0; i < issuePath.length; i++) { + const pathSegment = issuePath[i] + if (pathSegment === undefined) continue + + const segment = + typeof pathSegment === 'object' ? pathSegment.key : pathSegment + + // Standard Schema doesn't specify if paths should use numbers or stringified numbers for array access. + // However, if we follow the path it provides and encounter an array, then we can assume it's intended for array access. + const segmentAsNumber = Number(segment) + if (Array.isArray(currentFormValue) && !Number.isNaN(segmentAsNumber)) { + path += `[${segmentAsNumber}]` + } else { + path += (i > 0 ? '.' : '') + String(segment) + } + + if ( + typeof currentFormValue === 'object' && + currentFormValue !== null + ) { + currentFormValue = currentFormValue[segment as never] + } else { + currentFormValue = undefined + } + } schema.set(path, (schema.get(path) ?? []).concat(issue)) } @@ -46,8 +64,9 @@ function prefixSchemaToErrors(issues: readonly StandardSchemaV1Issue[]) { const transformFormIssues = ( issues: readonly StandardSchemaV1Issue[], + formValue: unknown, ): TStandardSchemaValidatorIssue => { - const schemaErrors = prefixSchemaToErrors(issues) + const schemaErrors = prefixSchemaToErrors(issues, formValue) return { form: schemaErrors, fields: schemaErrors, @@ -72,7 +91,7 @@ export const standardSchemaValidators = { if (validationSource === 'field') return result.issues as TStandardSchemaValidatorIssue - return transformFormIssues(result.issues) + return transformFormIssues(result.issues, value) }, async validateAsync( { @@ -87,7 +106,7 @@ export const standardSchemaValidators = { if (validationSource === 'field') return result.issues as TStandardSchemaValidatorIssue - return transformFormIssues(result.issues) + return transformFormIssues(result.issues, value) }, } diff --git a/packages/form-core/tests/standardSchemaValidator.spec.ts b/packages/form-core/tests/standardSchemaValidator.spec.ts index 501daf82f..7311b3897 100644 --- a/packages/form-core/tests/standardSchemaValidator.spec.ts +++ b/packages/form-core/tests/standardSchemaValidator.spec.ts @@ -431,35 +431,26 @@ describe('standard schema validator', () => { ]) }) - it('should handle string array indices correctly (Yup compatibility)', async () => { - // Mock a Standard Schema validator that returns string indices like Yup - const mockYupLikeValidator = { - '~standard': { - version: 1 as const, - vendor: 'mock-yup', - validate: (value: unknown) => { - const typedValue = value as { people?: { name?: string }[] } - if (!typedValue.people?.[0]?.name) { - return { - issues: [ - { - message: 'Name is required', - path: ['people', '0', 'name'], // String index like Yup - }, - ], - } - } - return { value: typedValue } - }, - }, - } + it('should handle string array indices from standard schema validators', async () => { + // Use Zod's superRefine to simulate string paths that some standard schema validators return + const schemaWithStringPaths = z.object({ + people: z.array(z.object({ + name: z.string() + })) + }).superRefine((_, ctx) => { + ctx.addIssue({ + code: 'custom', + message: 'Name is required', + path: ['people', '0', 'name'], // String index to test path handling + }) + }) const form = new FormApi({ defaultValues: { people: [{ name: '' }], }, validators: { - onChange: mockYupLikeValidator, + onChange: schemaWithStringPaths, }, }) @@ -554,5 +545,38 @@ describe('standard schema validator', () => { }, ]) }) + + it('should allow numeric object properties for standard schema issue paths', () => { + const form = new FormApi({ + defaultValues: { + foo: { + 0: { bar: '' }, + }, + }, + validators: { + onChange: z.object({ + foo: z.object({ + 0: z.object({ bar: z.string().email('Must be an email') }), + }), + }), + }, + }) + form.mount() + + const field = new FieldApi({ + form, + name: 'foo.0.bar', + }) + field.mount() + + field.setValue('test') + + expect(form.state.errors).toMatchObject([ + { 'foo.0.bar': [{ message: 'Must be an email' }] }, + ]) + expect(field.state.meta.errors).toMatchObject([ + { message: 'Must be an email' }, + ]) + }) }) }) From 0eee89a00cda9c5a6f45d09f71cdf552544b0d7e Mon Sep 17 00:00:00 2001 From: JIHOON LEE Date: Wed, 13 Aug 2025 01:26:31 +0900 Subject: [PATCH 4/5] refactor(react-core): simplify path construction in schema validation error handling --- packages/form-core/src/standardSchemaValidator.ts | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/packages/form-core/src/standardSchemaValidator.ts b/packages/form-core/src/standardSchemaValidator.ts index f175dea9c..fb953e4d2 100644 --- a/packages/form-core/src/standardSchemaValidator.ts +++ b/packages/form-core/src/standardSchemaValidator.ts @@ -27,21 +27,9 @@ function prefixSchemaToErrors( for (const issue of issues) { const issuePath = issue.path ?? [] - const path = [...(issue.path ?? [])] - .map((segment) => { - const normalizedSegment = - typeof segment === 'object' ? segment.key : segment - const isArrayIndex = - typeof normalizedSegment === 'number' || - (typeof normalizedSegment === 'string' && - /^\d+$/.test(normalizedSegment)) - - return isArrayIndex ? `[${normalizedSegment}]` : normalizedSegment - }) - .join('.') - .replace(/\.\[/g, '[') let currentFormValue = formValue + let path = '' for (let i = 0; i < issuePath.length; i++) { const pathSegment = issuePath[i] From 742868870f395edd3286b1d2bd6f182f00e8ebfc Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 13 Aug 2025 09:18:32 +0000 Subject: [PATCH 5/5] ci: apply automated fixes and generate docs --- .../form-core/src/standardSchemaValidator.ts | 7 ++---- .../tests/standardSchemaValidator.spec.ts | 24 +++++++++++-------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/packages/form-core/src/standardSchemaValidator.ts b/packages/form-core/src/standardSchemaValidator.ts index fb953e4d2..ae4b93c35 100644 --- a/packages/form-core/src/standardSchemaValidator.ts +++ b/packages/form-core/src/standardSchemaValidator.ts @@ -34,7 +34,7 @@ function prefixSchemaToErrors( for (let i = 0; i < issuePath.length; i++) { const pathSegment = issuePath[i] if (pathSegment === undefined) continue - + const segment = typeof pathSegment === 'object' ? pathSegment.key : pathSegment @@ -47,10 +47,7 @@ function prefixSchemaToErrors( path += (i > 0 ? '.' : '') + String(segment) } - if ( - typeof currentFormValue === 'object' && - currentFormValue !== null - ) { + if (typeof currentFormValue === 'object' && currentFormValue !== null) { currentFormValue = currentFormValue[segment as never] } else { currentFormValue = undefined diff --git a/packages/form-core/tests/standardSchemaValidator.spec.ts b/packages/form-core/tests/standardSchemaValidator.spec.ts index 5780809f1..ca0a7c78f 100644 --- a/packages/form-core/tests/standardSchemaValidator.spec.ts +++ b/packages/form-core/tests/standardSchemaValidator.spec.ts @@ -433,17 +433,21 @@ describe('standard schema validator', () => { it('should handle string array indices from standard schema validators', async () => { // Use Zod's superRefine to simulate string paths that some standard schema validators return - const schemaWithStringPaths = z.object({ - people: z.array(z.object({ - name: z.string() - })) - }).superRefine((_, ctx) => { - ctx.addIssue({ - code: 'custom', - message: 'Name is required', - path: ['people', '0', 'name'], // String index to test path handling + const schemaWithStringPaths = z + .object({ + people: z.array( + z.object({ + name: z.string(), + }), + ), + }) + .superRefine((_, ctx) => { + ctx.addIssue({ + code: 'custom', + message: 'Name is required', + path: ['people', '0', 'name'], // String index to test path handling + }) }) - }) const form = new FormApi({ defaultValues: {