Skip to content

Path-Scoped Validation for Forms and Interactive UIs

Full-object validation is still the right tool for submit boundaries:

javascript
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.

javascript
const profileSchema = createSchema({
  name: { type: 'string', required: true, minLength: 3 },
  role: { type: 'string', defaultTo: 'guest' }
})

profileSchema.validateAt('name', {
  name: '  Alex  '
})

Result:

javascript
{
  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.

javascript
profileSchema.validateAt('role', {}, { operation: 'create' })

Result:

javascript
{
  validatedValue: 'guest',
  errors: {}
}

If you want required checks for the exact selected field:

javascript
profileSchema.validateAt('name', {}, { operation: 'create' })

Result:

javascript
{
  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.

javascript
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:

javascript
workspaceSchema.validateAt('workspace.slug', {
  workspace: {
    slug: '  primary  '
  }
}, {
  operation: 'create'
})

Result:

javascript
{
  validatedValue: 'primary',
  errors: {}
}

Notice what did not happen:

  • workspace.id was not required
  • workspace.ownerUserId was 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:

javascript
workspaceSchema.validateAt('workspace', {
  workspace: {
    slug: '  primary  '
  }
}, {
  operation: 'create'
})

Result:

javascript
{
  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.slug validates one field
  • selecting workspace validates the whole nested object contract

validatePaths(paths, object, options)

Use validatePaths() when you want a subset of fields or a whole form step.

javascript
const stepSchema = createSchema({
  workspace: {
    type: 'object',
    schema: workspaceSummarySchema
  },
  status: { type: 'string', defaultTo: 'draft' }
})

stepSchema.validatePaths([
  'workspace.slug',
  'status'
], {
  workspace: {
    slug: '  next  '
  }
}, {
  operation: 'create'
})

Result:

javascript
{
  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:

javascript
workspaceSchema.validatePaths([
  'workspace.slug'
], {
  workspace: {
    slug: 'x'
  }
}, {
  operation: 'patch',
  skipParams: {
    'workspace.slug': ['minLength']
  }
})

mode also works as compatibility sugar for the built-in operations:

javascript
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() or validatePaths() to compute errors and normalized values
  • apply full normalization on submit with create(), replace(), or patch()

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()

GPL-3.0-only