diff --git a/packages/react-start-client/src/createServerFn.ts b/packages/react-start-client/src/createServerFn.ts index 5e9be52481..90cf8add39 100644 --- a/packages/react-start-client/src/createServerFn.ts +++ b/packages/react-start-client/src/createServerFn.ts @@ -1,6 +1,7 @@ import { default as invariant } from 'tiny-invariant' import { default as warning } from 'tiny-warning' import { isNotFound, isRedirect } from '@tanstack/react-router' +import { normalizeValidatorIssues } from '@tanstack/router-core' import { mergeHeaders } from './headers' import { globalMiddleware } from './registerGlobalMiddleware' import { startSerializer } from './serializer' @@ -802,8 +803,10 @@ function execValidator(validator: AnyValidator, input: unknown): unknown { if (result instanceof Promise) throw new Error('Async validation not supported') - if (result.issues) - throw new Error(JSON.stringify(result.issues, undefined, 2)) + if (result.issues) { + const issues = normalizeValidatorIssues(result.issues) + throw new Error(JSON.stringify(issues, undefined, 2)) + } return result.value } diff --git a/packages/router-core/src/index.ts b/packages/router-core/src/index.ts index 060b367c33..4dcc401fcd 100644 --- a/packages/router-core/src/index.ts +++ b/packages/router-core/src/index.ts @@ -312,6 +312,7 @@ export type { StrictOrFrom, } from './utils' +export { normalizeValidatorIssues } from './validators' export type { StandardSchemaValidatorProps, StandardSchemaValidator, diff --git a/packages/router-core/src/validators.ts b/packages/router-core/src/validators.ts index 9173080077..a9f852d10e 100644 --- a/packages/router-core/src/validators.ts +++ b/packages/router-core/src/validators.ts @@ -27,6 +27,13 @@ export interface AnyStandardSchemaValidateFailure { export interface AnyStandardSchemaValidateIssue { readonly message: string + readonly path?: + | ReadonlyArray + | undefined +} + +export interface AnyStandardSchemaValidatePathSegment { + readonly key: PropertyKey } export interface AnyStandardSchemaValidateInput { @@ -119,3 +126,59 @@ export type ResolveValidatorOutput = unknown extends TValidator : TValidator extends AnyValidatorObj ? ResolveValidatorOutputFn : ResolveValidatorOutputFn + +/** + * Creates and returns the dot path of an issue if possible. + * + * @param issue The issue to get the dot path from. + * + * @returns The dot path or null. + */ +function getDotPath(issue: AnyStandardSchemaValidateIssue): string | null { + if (issue.path?.length) { + let dotPath = '' + for (const item of issue.path) { + const key = typeof item === 'object' ? item.key : item + if (typeof key === 'string' || typeof key === 'number') { + if (dotPath) { + dotPath += `.${key}` + } else { + dotPath += key + } + } else { + return null + } + } + return dotPath + } + return null +} + +/** + * Extract spec-guaranteed issue's fields from validation results. + * + * @param issues Standard Schema validation issues. + * + * @returns Normalized issues, with root issues and issues by path. + */ +export function normalizeValidatorIssues( + issues: ReadonlyArray, +) { + const pathlessIssues: Array = [] + const issueMap: Record> = {} + + for (const issue of issues) { + const dotPath = getDotPath(issue) + if (dotPath) { + if (issueMap[dotPath]) { + issueMap[dotPath].push(issue.message) + } else { + issueMap[dotPath] = [issue.message] + } + } else { + pathlessIssues.push(issue.message) + } + } + + return { root: pathlessIssues, issues: issueMap } +}