BuzzForm
BuzzFormDocs

Validation

Learn about auto-derived validation, custom validators, and async validation in BuzzForm.

Validation

BuzzForm has two layers of validation:

  1. Auto-derived validators — generated from field properties like required, minLength, min, etc.
  2. Custom validators — defined in the validate config for additional rules

Auto-Derived Validation

Validation is automatically generated from field properties:

const contactSchema = defineSchema({
  fields: [
    {
      type: "text",
      name: "name",
      label: "Name",
      required: true,      // → adds "required" validator
      minLength: 2,        // → adds "minLength" validator
      maxLength: 50,       // → adds "maxLength" validator
    },
    {
      type: "email",
      name: "email",
      label: "Email",
      required: true,      // → adds "email" validator
    },
    {
      type: "textarea",
      name: "message",
      label: "Message",
      required: true,
      minLength: 10,       // → adds "minLength" validator
      maxLength: 500,      // → adds "maxLength" validator
    },
  ],
});

You don't need to manually specify these in validate — they're derived automatically.

Validation by Field Type

Field TypeAuto-Derived Validators
All fieldsrequired (if required: true)
text, textarea, passwordminLength, maxLength, pattern
emailemail
passwordpasswordCriteria (if criteria set)
numbermin, max, precision, step
dateminDate, maxDate
tagsminTags, maxTags
arrayminItems, maxItems
select (hasMany)minSelected, maxSelected

Custom Validation

Use the validate property for explicit validation rules beyond auto-derived validators. Checks are grouped by trigger:

const contactSchema = defineSchema({
  fields: [
    {
      type: "text",
      name: "name",
      label: "Name",
      required: true,
      minLength: 2,
      validate: {
        onBlur: {
          checks: [
            {
              type: "pattern",
              message: "Only letters and spaces allowed",
              args: { pattern: "^[a-zA-Z\\s]+$" },
            },
          ],
        },
      },
    },
    {
      type: "email",
      name: "email",
      label: "Email",
      required: true,
    },
    {
      type: "textarea",
      name: "message",
      label: "Message",
      required: true,
      minLength: 10,
      validate: {
        onSubmit: {
          checks: [
            {
              type: "pattern",
              message: "Message must contain at least one question",
              args: { pattern: ".*\\?.*" },
            },
          ],
        },
      },
    },
  ],
});

Validation Triggers

  • onChange — runs when the field value changes
  • onBlur — runs when the field loses focus
  • onSubmit — runs when the form is submitted

Async Validation

Use onBlur or onChange with async validators for server-side checks:

const registerSchema = defineSchema({
  fields: [
    {
      type: "text",
      name: "username",
      label: "Username",
      required: true,
      validate: {
        onBlur: {
          debounceMs: 500, // Wait 500ms after user stops typing
          checks: [
            {
              type: "usernameAvailable",
              message: "This username is already taken",
            },
          ],
        },
      },
    },
  ],
});

First, register the custom validator using defineValidators:

import { defineValidators } from "@buildnbuzz/form-react";

const customValidators = defineValidators({
  usernameAvailable: async (value: unknown) => {
    if (typeof value !== "string") return false;
    
    const response = await fetch(`/api/check-username?username=${value}`);
    const data = await response.json();
    return data.available ? true : "Username is already taken";
  },
});

const form = useForm({
  schema: registerSchema,
  registries: { validators: customValidators },
});

Use defineValidators to create type-safe custom validators. It provides proper typing for validator functions and ensures they return the correct format (true for valid, string for error message).

Dynamic Validation

BuzzForm supports full reactivity in validation. Both validator arguments and error messages can be dynamic expressions (Expr<T>).

Dynamic Validator Arguments

You can pass form data or context directly into validator arguments:

{
  type: "minLength",
  message: "Too short",
  // Re-validates whenever "/minChars" changes
  args: { 
    min: { $data: "/minChars" } 
  }
}

Dynamic Messages

Error messages can use template interpolation and conditional logic. They have access to data, context, and the resolved args of the current validator:

{
  type: "minLength",
  // Use resolved args in the message
  message: { $text: "Must be at least ${/args/min} characters." },
  args: { min: { $data: "/limit" } }
}

You can even use branching logic for messages:

{
  type: "required",
  message: {
    $when: { $data: "/isUrgent", eq: true },
    $then: "STOP! This is urgent and required.",
    $else: "Please provide a value."
  },
  args: { isRequired: true }
}

Cross-Field Validation

Use { $data: "/path" } to reference other fields:

const registerSchema = defineSchema({
  fields: [
    { type: "password", name: "password", label: "Password", required: true },
    {
      type: "password",
      name: "confirmPassword",
      label: "Confirm Password",
      required: true,
      validate: {
        onSubmit: {
          checks: [
            {
              type: "matches",
              message: "Passwords do not match",
              args: { other: { $data: "/password" } },
            },
          ],
        },
      },
    },
  ],
});

Important: Use JSON Pointer format (paths start with /). See Dynamic Values for details.

Built-in Validators

BuzzForm provides these built-in validators:

Prop

Type

Custom Validator Registry

Register custom validators globally using defineValidators:

import { defineValidators } from "@buildnbuzz/form-react";

// Define your custom validators with proper typing
const customValidators = defineValidators({
  usernameAvailable: async (value: unknown) => {
    if (typeof value !== "string") return false;
    
    const available = await checkUsernameAvailable(value);
    return available;
  },
  validatePromoCode: async (value: unknown) => {
    if (typeof value !== "string") return false;
    
    const valid = await validatePromo(value);
    return valid;
  },
});

// Pass to useForm
const form = useForm({
  schema: contactSchema,
  registries: { validators: customValidators },
});

Then use them in your schema:

const contactSchema = defineSchema({
  fields: [
    {
      type: "text",
      name: "username",
      validate: {
        onBlur: {
          checks: [
            {
              type: "usernameAvailable",
              message: "This username is taken",
            },
          ],
        },
      },
    },
  ],
});

Form-Level Validation

Add validation at the form level for complex rules involving multiple fields:

const contactSchema = defineSchema({
  fields: [
    { type: "text", name: "name", label: "Name" },
    { type: "email", name: "email", label: "Email" },
  ],
  validate: {
    onSubmit: {
      checks: [
        {
          type: "namesUnique",
          message: "All names must be unique",
          args: {
            fields: [{ $data: "/name" }, { $data: "/email" }],
          },
        },
      ],
    },
  },
});

Derived Validation Mode

Control when auto-derived validators run:

const form = useForm({
  schema,
  derivedValidationMode: "blur", // or "change" or "submit"
});
  • "blur" (default) — derived validators run on blur and submit
  • "change" — derived validators run on change, blur, and submit
  • "submit" — derived validators only run on submit

Complete Example

Building on the Quick Start contact form:

lib/schemas/contact.ts
import { defineSchema, type InferType } from "@buildnbuzz/form-react";

export const contactSchema = defineSchema({
  title: "Contact Form",
  fields: [
    {
      type: "text",
      name: "name",
      label: "Full Name",
      placeholder: "John Doe",
      required: true,
      minLength: 2,
      maxLength: 50,
      validate: {
        onBlur: {
          checks: [
            {
              type: "pattern",
              message: "Only letters and spaces allowed",
              args: { pattern: "^[a-zA-Z\\s]+$" },
            },
          ],
        },
      },
    },
    {
      type: "email",
      name: "email",
      label: "Email",
      placeholder: "john@example.com",
      required: true,
    },
    {
      type: "textarea",
      name: "message",
      label: "Message",
      placeholder: "How can we help?",
      required: true,
      minLength: 10,
      maxLength: 500,
      description: "Be as detailed as possible so we can help you better.",
    },
  ],
});

export type ContactData = InferType<typeof contactSchema.fields>;

On this page