# Implementing Form Validation in Lovable Projects

- Tool: Lovable
- Difficulty: Intermediate
- Fix time: ~15 min
- Compatibility: All Lovable projects
- Last updated: March 2026

## TL;DR

Form validation in Lovable projects works best with Zod schemas and react-hook-form. Define your validation rules in a Zod schema, connect it to react-hook-form with zodResolver, and display field-level error messages below each input. This approach validates on submit by default and gives users clear, specific feedback about what they need to fix.

## Why form validation needs explicit setup in Lovable apps

Lovable generates forms with basic HTML inputs and useState for tracking values, but it does not add validation logic by default. Without validation, users can submit empty fields, invalid email addresses, or passwords that are too short — and the form accepts everything silently.

HTML5 built-in validation (using required, type='email', pattern attributes) provides basic checks, but the error messages are browser-default and cannot be styled or customized. For a professional user experience, you need JavaScript validation that shows custom error messages inline, validates complex rules (like 'password must match confirmation'), and prevents submission until all fields are valid.

The recommended pattern for Lovable projects is Zod (for schema definition) combined with react-hook-form (for form state management). Zod lets you define validation rules as a schema that is also used for TypeScript types, keeping your validation and types in sync. React-hook-form manages form state efficiently and integrates with Zod through the zodResolver adapter.

- No validation logic — Lovable generates forms with useState but no validation rules
- HTML5 validation only — browser-default error messages are unstyleable and inconsistent across browsers
- Validation on every keystroke — causes poor UX when errors appear before the user finishes typing
- Missing field-level error messages — users do not know which specific field has the problem
- No type safety — form data is untyped, so TypeScript cannot catch invalid data shapes

## Error messages you might see

**Expected string, received undefined**

A Zod schema expected a string value but the form field was undefined (not initialized). Make sure all form fields have default values in the useForm defaultValues.

**String must contain at least 8 character(s)**

The Zod schema requires a minimum string length. This is a validation error message shown to the user — not a code bug. It means the validation is working correctly.

**Invalid email address**

The Zod .email() validator rejected the input. This is expected behavior — the user entered an invalid email format.

## Before you start

- A Lovable project with a form that needs validation
- The form's requirements (which fields are required, email format, password rules, etc.)
- Access to Dev Mode or the Lovable editor to modify form components

## How to fix it

### 1. Define a Zod validation schema

*Zod schemas define validation rules declaratively and also generate TypeScript types, keeping types and validation in sync*

Create a Zod schema that describes every field in your form with its validation rules. Each field gets a type (string, number, boolean) and constraints (min length, email format, required). The schema also serves as the TypeScript type for your form data — you extract the type with z.infer.

Before:

```
// No validation — form accepts any input
const [name, setName] = useState("");
const [email, setEmail] = useState("");

const handleSubmit = () => {
  // Sends whatever the user typed, no checks
  saveUser({ name, email });
};
```

After:

```
import { z } from "zod";

// Schema defines validation rules AND TypeScript types
const formSchema = z.object({
  name: z.string()
    .min(2, "Name must be at least 2 characters")
    .max(50, "Name must be under 50 characters"),
  email: z.string()
    .email("Please enter a valid email address"),
  password: z.string()
    .min(8, "Password must be at least 8 characters")
    .regex(/[A-Z]/, "Password must contain at least one uppercase letter")
    .regex(/[0-9]/, "Password must contain at least one number"),
});

// Extract the TypeScript type from the schema
type FormValues = z.infer<typeof formSchema>;
```

**Expected result:** A schema that validates all form fields and provides a TypeScript type for the form data.

### 2. Connect the schema to react-hook-form with zodResolver

*react-hook-form manages form state efficiently and zodResolver bridges it with your Zod validation schema*

Use the useForm hook from react-hook-form with the zodResolver adapter. Pass your Zod schema to the resolver. This connects the validation rules to the form — when the user submits, react-hook-form validates all fields against the schema and populates the errors object with any violations.

Before:

```
function SignupForm() {
  const [name, setName] = useState("");
  const [email, setEmail] = useState("");

  return (
    <form onSubmit={() => saveUser({ name, email })}>
      <input value={name} onChange={e => setName(e.target.value)} />
      <input value={email} onChange={e => setEmail(e.target.value)} />
      <button type="submit">Sign Up</button>
    </form>
  );
}
```

After:

```
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";

function SignupForm() {
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting },
  } = useForm<FormValues>({
    resolver: zodResolver(formSchema),
    defaultValues: {
      name: "",
      email: "",
      password: "",
    },
  });

  const onSubmit = async (data: FormValues) => {
    // data is fully validated and typed at this point
    await saveUser(data);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register("name")} placeholder="Full name" />
      {errors.name && <p className="text-destructive text-sm">{errors.name.message}</p>}

      <input {...register("email")} placeholder="Email" />
      {errors.email && <p className="text-destructive text-sm">{errors.email.message}</p>}

      <input {...register("password")} type="password" placeholder="Password" />
      {errors.password && <p className="text-destructive text-sm">{errors.password.message}</p>}

      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? "Signing up..." : "Sign Up"}
      </button>
    </form>
  );
}
```

**Expected result:** The form validates on submit. Invalid fields show error messages below them. Valid data is passed to onSubmit fully typed.

### 3. Use shadcn/ui Form components for styled validation

*shadcn/ui provides pre-built Form components that integrate with react-hook-form and display errors consistently*

Replace plain HTML inputs with shadcn/ui Form components. These components handle the register binding, error display, and styling automatically. They provide FormField, FormItem, FormLabel, FormControl, FormMessage, and FormDescription components that wrap react-hook-form's functionality with consistent Tailwind styling.

Before:

```
<input {...register("email")} placeholder="Email" />
{errors.email && <p className="text-destructive text-sm">{errors.email.message}</p>}
```

After:

```
import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from "@/components/ui/form";
import { Input } from "@/components/ui/input";

<FormField
  control={form.control}
  name="email"
  render={({ field }) => (
    <FormItem>
      <FormLabel>Email</FormLabel>
      <FormControl>
        <Input placeholder="you@example.com" {...field} />
      </FormControl>
      <FormMessage /> {/* Automatically shows the Zod error message */}
    </FormItem>
  )}
/>
```

**Expected result:** Form fields have consistent labels, styling, and error messages that match the rest of your shadcn/ui design.

### 4. Add cross-field validation for related fields

*Some validation rules depend on multiple fields, like confirming a password matches*

Zod supports the .refine() method for cross-field validation that compares values between fields. Add a refinement to the entire schema object that checks if password and confirmPassword match. If your form validation involves complex business rules, conditional required fields, or multi-step form logic, RapidDev's engineers have built these patterns across 600+ Lovable projects.

Before:

```
const schema = z.object({
  password: z.string().min(8),
  confirmPassword: z.string().min(8),
});
```

After:

```
const schema = z.object({
  password: z.string().min(8, "Password must be at least 8 characters"),
  confirmPassword: z.string(),
}).refine((data) => data.password === data.confirmPassword, {
  message: "Passwords do not match",
  path: ["confirmPassword"], // Error appears on the confirmPassword field
});
```

**Expected result:** If the two password fields do not match, an error message appears below the confirm password field.

## Complete code example

File: `src/components/ContactForm.tsx`

```typescript
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from "@/components/ui/form";
import { useToast } from "@/hooks/use-toast";

const contactSchema = z.object({
  name: z.string().min(2, "Name must be at least 2 characters"),
  email: z.string().email("Please enter a valid email address"),
  subject: z.string().min(5, "Subject must be at least 5 characters"),
  message: z.string().min(20, "Message must be at least 20 characters").max(1000, "Message must be under 1000 characters"),
});

type ContactFormValues = z.infer<typeof contactSchema>;

export function ContactForm() {
  const { toast } = useToast();
  const form = useForm<ContactFormValues>({
    resolver: zodResolver(contactSchema),
    defaultValues: { name: "", email: "", subject: "", message: "" },
  });

  const onSubmit = async (data: ContactFormValues) => {
    // data is validated and typed — safe to send
    toast({ title: "Message sent", description: "We will get back to you soon." });
    form.reset();
  };

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4 max-w-md">
        <FormField control={form.control} name="name" render={({ field }) => (
          <FormItem>
            <FormLabel>Name</FormLabel>
            <FormControl><Input placeholder="Your name" {...field} /></FormControl>
            <FormMessage />
          </FormItem>
        )} />
        <FormField control={form.control} name="email" render={({ field }) => (
          <FormItem>
            <FormLabel>Email</FormLabel>
            <FormControl><Input type="email" placeholder="you@example.com" {...field} /></FormControl>
            <FormMessage />
          </FormItem>
        )} />
        <FormField control={form.control} name="subject" render={({ field }) => (
          <FormItem>
            <FormLabel>Subject</FormLabel>
            <FormControl><Input placeholder="What is this about?" {...field} /></FormControl>
            <FormMessage />
          </FormItem>
        )} />
        <FormField control={form.control} name="message" render={({ field }) => (
          <FormItem>
            <FormLabel>Message</FormLabel>
            <FormControl><Textarea placeholder="Your message..." rows={5} {...field} /></FormControl>
            <FormMessage />
          </FormItem>
        )} />
        <Button type="submit" disabled={form.formState.isSubmitting}>
          {form.formState.isSubmitting ? "Sending..." : "Send Message"}
        </Button>
      </form>
    </Form>
  );
}
```

## Best practices

- Use Zod + react-hook-form + zodResolver as the standard validation pattern for all Lovable forms
- Always provide default values for every form field to prevent 'undefined' validation errors
- Validate on submit (the default) rather than on every keystroke — it creates a better user experience
- Show error messages below each individual field using FormMessage, not in a single list at the top of the form
- Disable the submit button while the form is submitting to prevent double submissions
- Use .refine() for cross-field validation like password confirmation, conditional required fields, or date ranges
- Extract form schemas into a separate file (like src/schemas/contact.ts) so they can be reused for API validation

## Frequently asked questions

### How do I add form validation to a Lovable project?

Use Zod to define a validation schema, then connect it to react-hook-form using zodResolver. Register each form field with the register function or use shadcn/ui FormField components. Error messages appear automatically when the user submits invalid data.

### Should I validate on every keystroke or on submit?

Validate on submit (the default with react-hook-form). Validating on every keystroke shows errors before the user finishes typing, which is frustrating. After the first submission attempt, react-hook-form switches to validate on change so errors clear as the user fixes them.

### How do I validate that two password fields match?

Use Zod's .refine() method on the schema object: .refine((data) => data.password === data.confirmPassword, { message: 'Passwords do not match', path: ['confirmPassword'] }). The path option controls which field shows the error.

### Can I use shadcn/ui form components with react-hook-form?

Yes, and this is the recommended approach. shadcn/ui provides Form, FormField, FormItem, FormLabel, FormControl, and FormMessage components that integrate directly with react-hook-form and display errors with consistent Tailwind styling.

### How do I show a success message after form submission?

Use the useToast hook from shadcn/ui to display a toast notification after successful submission. Call form.reset() to clear the form. Example: toast({ title: 'Message sent', description: 'We will reply within 24 hours.' }).

### What if I can't fix this myself?

If your form involves complex validation logic like conditional required fields, multi-step flows, or server-side validation integration, RapidDev's engineers have built these patterns across 600+ Lovable projects and can implement production-ready form validation.

---

Source: https://www.rapidevelopers.com/lovable-issues/implementing-form-validation-in-lovable-projects
© RapidDev — https://www.rapidevelopers.com/lovable-issues/implementing-form-validation-in-lovable-projects
