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