Path-Scoped Validation for Forms and Interactive UIs
Full-object validation is still the right tool for submit boundaries:
const result = userSchema.create(payload)But forms often need something narrower:
- validate one field on blur
- validate a small step in a wizard
- normalize only the field the user just touched
- avoid triggering unrelated sibling errors while the user is still editing
That is what validateAt() and validatePaths() are for.
validateAt(path, object, options)
Use validateAt() when you want one path.
const profileSchema = createSchema({
name: { type: 'string', required: true, minLength: 3 },
role: { type: 'string', defaultTo: 'guest' }
})
profileSchema.validateAt('name', {
name: ' Alex '
})Result:
{
validatedValue: 'Alex',
errors: {}
}By default, path validation uses patch semantics. That means:
- only the selected path is validated
- missing required siblings do not produce errors
- defaults do not apply unless you explicitly choose an operation that applies them
If you want create-style or replace-style behavior for the exact selected path, pass operation.
profileSchema.validateAt('role', {}, { operation: 'create' })Result:
{
validatedValue: 'guest',
errors: {}
}If you want required checks for the exact selected field:
profileSchema.validateAt('name', {}, { operation: 'create' })Result:
{
validatedValue: undefined,
errors: {
name: {
field: 'name',
code: 'REQUIRED',
message: 'Field is required',
params: {}
}
}
}Nested path example
This is where path-scoped validation becomes most useful.
const workspaceSummarySchema = createSchema({
id: { type: 'id', required: true },
slug: { type: 'string', required: true, minLength: 3 },
ownerUserId: { type: 'id', required: true }
})
const workspaceSchema = createSchema({
workspace: {
type: 'object',
required: true,
schema: workspaceSummarySchema
}
})Validate only workspace.slug:
workspaceSchema.validateAt('workspace.slug', {
workspace: {
slug: ' primary '
}
}, {
operation: 'create'
})Result:
{
validatedValue: 'primary',
errors: {}
}Notice what did not happen:
workspace.idwas not requiredworkspace.ownerUserIdwas not required- unrelated nested keys were not validated
That is the point of the API. It validates the selected path, not the whole object.
If you select the whole object path instead:
workspaceSchema.validateAt('workspace', {
workspace: {
slug: ' primary '
}
}, {
operation: 'create'
})Result:
{
validatedValue: {
slug: 'primary'
},
errors: {
'workspace.id': {
field: 'workspace.id',
code: 'REQUIRED',
message: 'Field is required',
params: {}
},
'workspace.ownerUserId': {
field: 'workspace.ownerUserId',
code: 'REQUIRED',
message: 'Field is required',
params: {}
}
}
}That distinction is intentional:
- selecting
workspace.slugvalidates one field - selecting
workspacevalidates the whole nested object contract
validatePaths(paths, object, options)
Use validatePaths() when you want a subset of fields or a whole form step.
const stepSchema = createSchema({
workspace: {
type: 'object',
schema: workspaceSummarySchema
},
status: { type: 'string', defaultTo: 'draft' }
})
stepSchema.validatePaths([
'workspace.slug',
'status'
], {
workspace: {
slug: ' next '
}
}, {
operation: 'create'
})Result:
{
validatedObject: {
workspace: {
slug: 'next'
},
status: 'draft'
},
errors: {}
}This is useful for:
- wizard-step validation
- validating only dirty fields
- validating a form section before moving on
Path options and compatibility
Path-scoped validation supports the same flat nested option model:
workspaceSchema.validatePaths([
'workspace.slug'
], {
workspace: {
slug: 'x'
}
}, {
operation: 'patch',
skipParams: {
'workspace.slug': ['minLength']
}
})mode also works as compatibility sugar for the built-in operations:
workspaceSchema.validateAt('workspace.slug', values, { mode: 'patch' })Form integration guidance
These APIs are meant to help form adapters, but the library still does not become a form framework.
Recommended approach:
- keep raw input state in the UI while the user is typing
- use
validateAt()orvalidatePaths()to compute errors and normalized values - apply full normalization on submit with
create(),replace(), orpatch()
That matters because aggressive normalization during typing can be annoying:
- trimming on every keypress can move the cursor
- number coercion can fight half-finished input such as
12. - nested defaults can appear before the user has actually submitted anything
So the intended split is:
- interactive validation:
validateAt()/validatePaths() - submit boundary validation:
create()/replace()/patch()