Skip to main content

Validation

Formwright supports two validation patterns: schema-driven field rules that map to React Hook Form, and custom resolvers (Zod, Yup, or any RHF-compatible resolver).

Schema-driven validation​

Define constraints directly on fields when you build the schema:

import { field } from "formwright/schema";

const email = field.email("email", {
label: "Email",
required: true,
});

const password = field.text("password", {
label: "Password",
required: true,
minLength: 8,
maxLength: 64,
});

const age = field.integer("age", {
label: "Age",
minimum: 18,
maximum: 120,
});

const website = field.url("website", {
label: "Website",
pattern: "^https://",
});

Formwright maps these constraints to React Hook Form validation rules automatically. Errors appear in the field's error prop.

Using schema rules to require fields conditionally​

Combine schema constraints with behavior rules for conditional validation:

import { rule, fieldRef } from "formwright/schema";

rules: [
// Required only when accountType is "company"
rule.when(fieldRef(accountType).eq("company")).require(companyName),
]

This updates the field's required state at runtime — no custom validator needed.

Custom resolver (Zod)​

For cross-field validation, complex constraints, or server-error mapping, pass a custom resolver to FormRuntimeProvider:

import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { FormRuntimeProvider, FormRuntimeRoot } from "formwright/react";

const schema = z
.object({
email: z.string().email("Enter a valid email"),
password: z.string().min(8, "At least 8 characters"),
confirmPassword: z.string(),
})
.refine(
(values) => values.password === values.confirmPassword,
{ path: ["confirmPassword"], message: "Passwords do not match" }
);

export function SignupForm({ runtime }: { runtime: FormRuntime }) {
return (
<FormRuntimeProvider runtime={runtime} validationResolver={zodResolver(schema)}>
<FormRuntimeRoot rootLayoutId="root" />
</FormRuntimeProvider>
);
}

Install the resolver:

npm install zod @hookform/resolvers

Custom resolver (Yup)​

import * as yup from "yup";
import { yupResolver } from "@hookform/resolvers/yup";

const schema = yup.object({
email: yup.string().email("Invalid email").required("Required"),
age: yup.number().min(18, "Must be 18+").required("Required"),
});

<FormRuntimeProvider runtime={runtime} validationResolver={yupResolver(schema)}>

Wire errors in custom renderers​

When you write a custom FieldRendererComponent, always render the error prop so validation feedback reaches the user:

import type { FieldRendererComponent } from "formwright/react";

const MyInput: FieldRendererComponent = ({ value, onChange, onBlur, error }) => (
<div>
<input
value={typeof value === "string" ? value : ""}
onChange={(e) => onChange(e.target.value)}
onBlur={onBlur}
aria-invalid={Boolean(error)}
/>
{error && <p className="text-sm text-red-600">{error}</p>}
</div>
);

Using toRHFValidationRules in custom renderers​

If you're building a custom renderer that uses useController directly, convert field schema constraints to RHF rules:

import { toRHFValidationRules, useFormField } from "formwright/react";
import { useController } from "react-hook-form";

function CustomField({ path }: { path: string }) {
const { field, state } = useFormField(path);
const rules = toRHFValidationRules(field.definition, state.required);

const { field: controller } = useController({ name: path, rules });

return <input {...controller} />;
}

Most custom renderers don't need this — useFormField wires RHF automatically.