Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
9 changes: 6 additions & 3 deletions packages/form-core/src/standardSchemaValidator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,12 @@ function prefixSchemaToErrors(issues: readonly StandardSchemaV1Issue[]) {
.map((segment) => {
const normalizedSegment =
typeof segment === 'object' ? segment.key : segment
return typeof normalizedSegment === 'number'
? `[${normalizedSegment}]`
: normalizedSegment
const isArrayIndex =
typeof normalizedSegment === 'number' ||
(typeof normalizedSegment === 'string' &&
/^\d+$/.test(normalizedSegment))

return isArrayIndex ? `[${normalizedSegment}]` : normalizedSegment
})
.join('.')
.replace(/\.\[/g, '[')
Expand Down
157 changes: 157 additions & 0 deletions packages/form-core/tests/standardSchemaValidator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' }],
},
])
})
})
})