Problem
Some fields are one field from the form/UI perspective, but their value is a nested structure.
Examples:
const Schema = v.object({
// Tiptap / ProseMirror JSON
content: TiptapContentSchema,
// Editor.js output
article: EditorJsOutputSchema,
// Reka UI Tags Input or similar tag editor
tags: v.array(TagSchema),
// File upload field with per-file metadata
files: v.array(FileWithMetadataSchema),
});
These are usually not separate form fields. They are one custom field connected with useField() or <Field>:
<Field :of="form" :path="['content']" v-slot="field">
<TiptapEditor v-model="field.input" />
<div v-if="field.errors?.[0]">
{{ field.errors[0] }}
</div>
</Field>
<Field :of="form" :path="['tags']" v-slot="field">
<TagsInputRoot v-model="field.input">
<!-- tags input implementation -->
</TagsInputRoot>
<div v-if="field.errors?.[0]">
{{ field.errors[0] }}
</div>
</Field>
<Field :of="form" :path="['files']" v-slot="field">
<FileUploadField v-model="field.input" />
<div v-if="field.errors?.[0]">
{{ field.errors[0] }}
</div>
</Field>
The problem is that validation can fail deeper inside the value:
content.content.0.content.1.text
article.blocks.3.data.items.0.content
tags.2.label
files.0.metadata.crop.width
But from the form perspective, the error often needs to be displayed at the parent field:
content
article
tags
files
The user does not interact with content.content.0.content.1.text as a separate field. They interact with one Tiptap editor.
The same applies to Editor.js, tags input, and upload fields with nested metadata.
Current limitation
Formisch already has getDeepErrors and getDeepErrorEntries, but for this use case the field usually only needs the first relevant deep error.
Using getDeepErrors(...)[0] is not ideal because it suggests collecting all descendant errors just to render one message.
The missing primitive is a way to ask:
What is the first error for this field, including errors below this field path?
For example:
getDeepError(form, { path: ['content'] })
getDeepError(form, { path: ['article'] })
getDeepError(form, { path: ['tags'] })
getDeepError(form, { path: ['files'] })
Expected behavior:
- Check direct errors at the requested path.
- Search descendant errors.
- Return the first error.
- Stop searching after the first match.
Then a field could be written like this:
<Field :of="form" :path="['content']" v-slot="field">
<TiptapEditor v-model="field.input" />
<div v-if="field.errors?.[0] || getDeepError(form, { path: ['content'] })">
{{ field.errors?.[0] ?? getDeepError(form, { path: ['content'] }) }}
</div>
</Field>
A related helper could also be useful:
getDeepErrorEntry(form, { path: ['content'] })
returning the original path together with the message:
{
path: ['content', 'content', 0, 'content', 1, 'text'],
error: 'Content is required'
}
Possible Valibot-level solution
This could also be solved at the schema level.
Valibot could provide a way to mark a schema as an error boundary / error surface:
const Schema = v.object({
content: v.errorBoundary(TiptapContentSchema, {
mode: 'first',
}),
article: v.errorBoundary(EditorJsOutputSchema, {
mode: 'first',
}),
tags: v.errorBoundary(v.array(TagSchema), {
mode: 'first',
}),
files: v.errorBoundary(v.array(FileWithMetadataSchema), {
mode: 'first',
}),
});
Then an issue at:
could be surfaced as:
For example, flattened errors could become:
{
nested: {
tags: ['Tag label is required']
}
}
instead of only:
{
nested: {
'tags.2.label': ['Tag label is required']
}
}
Possible behavior:
type ErrorBoundaryOptions = {
mode?: 'first' | 'all' | number;
message?: string | ((issues: BaseIssue[]) => string);
preserveIssues?: boolean;
};
Examples:
v.errorBoundary(schema, { mode: 'first' })
Surface only the first deep issue at the parent path.
v.errorBoundary(schema, { mode: 'all' })
Surface all deep issue messages at the parent path.
v.errorBoundary(schema, { message: 'Invalid content' })
Use one parent-level message instead of exposing internal paths/messages.
Suggested direction
This could be solved in Formisch, Valibot, or both.
On the Formisch side, an optimized helper like this would solve the immediate UI problem:
getDeepError(form, { path })
getDeepErrorEntry(form, { path })
On the Valibot side, an error boundary / error surface would solve it semantically:
v.errorBoundary(schema, { mode: 'first' })
I think the Valibot-level solution is the cleaner long-term solution because the schema author knows whether a nested value should behave as one field.
The Formisch-level helper is still useful because field components often need a simple and performant way to display the first descendant error for a field.
Problem
Some fields are one field from the form/UI perspective, but their value is a nested structure.
Examples:
These are usually not separate form fields. They are one custom field connected with
useField()or<Field>:The problem is that validation can fail deeper inside the value:
But from the form perspective, the error often needs to be displayed at the parent field:
The user does not interact with
content.content.0.content.1.textas a separate field. They interact with one Tiptap editor.The same applies to Editor.js, tags input, and upload fields with nested metadata.
Current limitation
Formisch already has
getDeepErrorsandgetDeepErrorEntries, but for this use case the field usually only needs the first relevant deep error.Using
getDeepErrors(...)[0]is not ideal because it suggests collecting all descendant errors just to render one message.The missing primitive is a way to ask:
For example:
Expected behavior:
Then a field could be written like this:
A related helper could also be useful:
returning the original path together with the message:
Possible Valibot-level solution
This could also be solved at the schema level.
Valibot could provide a way to mark a schema as an error boundary / error surface:
Then an issue at:
could be surfaced as:
tagsFor example, flattened errors could become:
instead of only:
Possible behavior:
Examples:
Surface only the first deep issue at the parent path.
Surface all deep issue messages at the parent path.
Use one parent-level message instead of exposing internal paths/messages.
Suggested direction
This could be solved in Formisch, Valibot, or both.
On the Formisch side, an optimized helper like this would solve the immediate UI problem:
On the Valibot side, an error boundary / error surface would solve it semantically:
I think the Valibot-level solution is the cleaner long-term solution because the schema author knows whether a nested value should behave as one field.
The Formisch-level helper is still useful because field components often need a simple and performant way to display the first descendant error for a field.