Skip to content

fix(form-core): handle string array indices in prefixSchemaToErrors #1689

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from

Conversation

jiji-hoon96
Copy link

Description

This PR fixes a bug in the prefixSchemaToErrors function within standardSchemaValidator.ts where array indices passed as strings (like '0', '1') were not being properly converted to bracket notation, causing validation errors for array fields to be incorrectly formatted.

Problem

  • Standard Schema validation libraries handle array indices differently:
    • Zod: passes numeric indices (0, 1, 2)
    • Yup: passes string indices ('0', '1', '2')
  • The existing code only checked for typeof normalizedSegment === 'number', missing string-based indices
  • This caused paths like people.0.name instead of the correct people[0].name

Solution

  • Enhanced the array index detection logic to handle both numeric and string indices
  • Added regex pattern /^\d+$/ to identify string representations of numbers
  • Maintained backward compatibility with existing numeric index handling
  • Ensured TypeScript type safety throughout the changes

Related Issue

Fixes #1683

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

Testing

  • Added comprehensive test cases covering:
    • Numeric array indices (Zod compatibility)
    • String array indices (Yup compatibility)
    • Nested arrays with mixed index types
    • Regular object paths without arrays
  • All existing tests continue to pass
  • New tests verify the fix works correctly

Test Results

✓ standard schema validator > array path handling > should handle numeric array indices correctly
✓ standard schema validator > array path handling > should handle string array indices correctly (Yup compatibility)  
✓ standard schema validator > array path handling > should handle nested arrays with mixed numeric and string indices
✓ standard schema validator > array path handling > should handle regular object paths without array indices

Code Changes

Before

return typeof normalizedSegment === 'number'
  ? `[${normalizedSegment}]`
  : normalizedSegment

After

// Check if segment is a number or a string that represents a valid array index
const isArrayIndex = 
  typeof normalizedSegment === 'number' ||
  (typeof normalizedSegment === 'string' && /^\d+$/.test(normalizedSegment))

return isArrayIndex
  ? `[${normalizedSegment}]`
  : normalizedSegment

Impact

  • ✅ Fixes array field validation error display issues
  • ✅ Maintains compatibility with all existing Standard Schema implementations
  • ✅ No breaking changes to existing functionality
  • ✅ Improves developer experience when using array validations

Checklist

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation (if applicable)
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published in downstream modules

- Fix array path handling for Standard Schema validators
- Support both numeric (Zod) and string (Yup) array indices

Fixes TanStack#1683
Copy link

nx-cloud bot commented Aug 12, 2025

View your CI Pipeline Execution ↗ for commit b3f6cd1

Command Status Duration Result
nx affected --targets=test:sherif,test:knip,tes... ✅ Succeeded 1m 21s View ↗
nx run-many --target=build --exclude=examples/** ✅ Succeeded 20s View ↗

☁️ Nx Cloud last updated this comment at 2025-08-12 08:41:55 UTC

Copy link

pkg-pr-new bot commented Aug 12, 2025

More templates

@tanstack/angular-form

npm i https://pkg.pr.new/@tanstack/angular-form@1689

@tanstack/form-core

npm i https://pkg.pr.new/@tanstack/form-core@1689

@tanstack/lit-form

npm i https://pkg.pr.new/@tanstack/lit-form@1689

@tanstack/react-form

npm i https://pkg.pr.new/@tanstack/react-form@1689

@tanstack/solid-form

npm i https://pkg.pr.new/@tanstack/solid-form@1689

@tanstack/svelte-form

npm i https://pkg.pr.new/@tanstack/svelte-form@1689

@tanstack/vue-form

npm i https://pkg.pr.new/@tanstack/vue-form@1689

commit: b3f6cd1

Copy link

codecov bot commented Aug 12, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
⚠️ Please upload report for BASE (main@9e092e2). Learn more about missing BASE report.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #1689   +/-   ##
=======================================
  Coverage        ?   90.58%           
=======================================
  Files           ?       37           
  Lines           ?     1689           
  Branches        ?      422           
=======================================
  Hits            ?     1530           
  Misses          ?      142           
  Partials        ?       17           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Contributor

@LeCarbonator LeCarbonator left a comment

Choose a reason for hiding this comment

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

Looks promising! Thanks for tackling the issue. Let me know if you need more info or help with the PR.

Comment on lines 30 to 36
const isArrayIndex =
typeof normalizedSegment === 'number' ||
(typeof normalizedSegment === 'string' &&
/^\d+$/.test(normalizedSegment))

return isArrayIndex ? `[${normalizedSegment}]` : normalizedSegment
})
Copy link
Contributor

Choose a reason for hiding this comment

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

This breaks for numeric properties (example: foo.01234.bar). Ideally, we also need unit tests to ensure they don't break with this implementation.

However, the standard schema validator has access to the form value it validated, so we can use it as reference to see if the access is an array or not.

Something like this:

function prefixSchemaToErrors(
  issues: readonly StandardSchemaV1Issue[],
  formValue: unknown,
) {
  const schema = new Map<string, StandardSchemaV1Issue[]>()

  for (const issue of issues) {
    const issuePath = issue.path ?? []

    let currentFormValue = formValue
    let path = ''

    for (let i = 0; i < issuePath.length; i++) {
      const pathSegment = issuePath[i]!
      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) && !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))
  }

  return Object.fromEntries(schema)
}

])
})

it('should handle string array indices correctly (Yup compatibility)', async () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Nitpick, but this is less about Yup being special, and more about standard schema not specifying what a path should contain. It could be any library implementing standard schema.

Comment on lines 436 to 455
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 }
},
},
}
Copy link
Contributor

Choose a reason for hiding this comment

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

There's no need to mock a validator.

Zod allows you to set a custom path, allowing us to imitate string paths.

z.object({ 
   people: z.array(z.object({ 
       name: z.string() 
   }))
}).superRefine((_, ctx) => {
    ctx.addIssue({
        code: 'custom',
        message: 'Name is required',
        path: [ 'people', '0', 'name' ],
    })
})

},
])
})
})
Copy link
Contributor

Choose a reason for hiding this comment

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

Failing unit test for the numeric property comment above:

  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' },
    ])
  })

@jiji-hoon96
Copy link
Author

Thank you for the excellent feedback! Your suggestions were spot-on and made the implementation much more robust.

I really appreciate you pointing out the numeric object property issue and providing the better approach using actual form values as reference. The Array.isArray(currentFormValue) solution is much more reliable than my initial regex approach.

I've committed all the changes incorporating your feedback

  • ✅ Form value reference instead of regex detection
  • ✅ Updated test descriptions and replaced mocks with Zod's superRefine
  • ✅ Added the numeric property test case
  • ✅ All tests passing

Please review when you have a chance. Thanks for the great mentorship!

@dlindahl
Copy link

Appreciate the work here, but something made me wonder... in the interest of openness, has this PR been created by AI by any chance?

@jiji-hoon96
Copy link
Author

I wrote the bug fix logic myself. However, I did refer to some reference materials while searching for an efficient way to implement the code. Could this be a problem?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

prefixSchemaToErrors does not support paths for arrays of objects
3 participants