21

Cross-field validation

Validate one field against another, such as confirm password or date ranges, and keep the dependent field's error up to date.

Some rules involve more than one field: a confirmation must match, an end date must follow a start date. NotForm validates with your schema, so you write the rule in the schema and tell it which field should display the error.

Put the rule on the schema, and set the path

An issue is shown by the field whose path matches the issue's path. Report the issue on the field you want to display it.

import { z } from 'zod'

const schema = z.object({
  confirmPassword: z.string().min(1, 'Confirm your password.'),
  password: z.string().min(8, 'Use at least 8 characters.'),
}).refine((values) => {
  return values.password === values.confirmPassword
}, {
  message: 'Passwords do not match.',
  path: ['confirmPassword'],
})

Without path, the issue is form-level and no field displays it.

Some validators skip object-level rules while a field in the object is invalid, so the mismatch error may only appear after the individual fields pass. Check your validator's documentation if the cross-field message does not show when you expect it.

Keep the dependent field up to date

Fields validate on their own events. If the user fixes password after confirmPassword showed an error, confirmPassword does not know to re-validate. Watch the source field and re-validate the dependent one.

<script setup lang="ts">
import { watch } from 'vue'

const password = form.field('password')
const confirmPassword = form.field('confirmPassword')

watch(
  () => {
    return password.model
  },
  () => {
  // only after the user has interacted with the confirm field
    if (confirmPassword.isTouched) {
      void confirmPassword.validate()
    }
  }
)
</script>

<template>
  <Form
    :form="form"
    novalidate
  >
    <Field
      v-slot="{ field }"
      :field="password"
    >
      <label :for="field.path">Password</label>

      <input
        :id="field.path"
        v-model="field.model"
        v-bind="field.events"
        type="password"
      >

      <FieldError :errors="field" />
    </Field>

    <Field
      v-slot="{ field }"
      :field="confirmPassword"
    >
      <label :for="field.path">Confirm password</label>

      <input
        :id="field.path"
        v-model="field.model"
        v-bind="field.events"
        type="password"
      >

      <FieldError :errors="field" />
    </Field>
  </Form>
</template>

Passing the pre-created field instances to Field is fine. Field accepts any field instance.

Date ranges

The same pattern applies to any dependent pair.

const schema = z.object({
  endDate: z.string().min(1, 'Choose an end date.'),
  startDate: z.string().min(1, 'Choose a start date.'),
}).refine((values) => {
  return values.endDate >= values.startDate
}, {
  message: 'End date must be on or after the start date.',
  path: ['endDate'],
})

ISO date strings (2026-11-02) compare correctly as text. Watch startDate and re-validate endDate, exactly as above.

Show a rule for the whole form

For a rule that is not about one field, omit path and show it as a form-level error. See Errors.

Copyright © 2026 NotForm
Published under MIT License