---
title: "Cross-field validation"
description: "Validate one field against another, such as confirm password or date ranges, and keep the dependent field's error up to date."
canonical_url: "https://notformdocs.vercel.app/docs/recipes/cross-field-validation"
---

> Index of every page on this site: [llms.txt](https://notformdocs.vercel.app/llms.txt)

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.

```ts
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.

<note>

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.

</note>

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

<code-collapse>

```vue
<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>
```

</code-collapse>

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.

```ts
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](https://notformdocs.vercel.app/docs/guides/errors#show-form-level-errors).

## Sitemap

See the full [sitemap](https://notformdocs.vercel.app/sitemap.md) for all pages.
