modular-forms icon indicating copy to clipboard operation
modular-forms copied to clipboard

Support for Zod 4

Open jtmueller opened this issue 2 months ago • 4 comments

I would love to see a version that's compatible with Zod 4. It's currently the only thing preventing me from upgrading Zod in my project, and I'd very much like to upgrade Zod to take advantage of the dramatic performance and bundle-size improvements.

If I try to update to Zod 4, every single call to zodForm raises TypeScript errors such as:

error TS2345: Argument of type 'ZodObject<{ email: ZodString; password: ZodString; }, $strip>' is not assignable to parameter of type 'ZodType<any, any, FieldValues>'.
  Types of property 'def' are incompatible.
    Type '$ZodObjectDef<{ email: ZodString; password: ZodString; }>' is not assignable to type 'FieldValues | FieldValue | (FieldValues | FieldValue)[]'.
      Type '$ZodObjectDef<{ email: ZodString; password: ZodString; }>' is not assignable to type 'FieldValues'.
        Index signature for type 'string' is missing in type '$ZodObjectDef<{ email: ZodString; password: ZodString; }>'.

93     validate: zodForm(LoginVerificationSchema),

jtmueller avatar Sep 29 '25 22:09 jtmueller

Hey, the zodForm function is a really simple function. Here is the code. I recommend copy it to your project and upgrade it as I am more focused on Formisch right now.

fabian-hiller avatar Oct 03 '25 02:10 fabian-hiller

@fabian-hiller Thanks - if it helps at all, this version of the function appears to type-check and work with both Zod 3.x and Zod 4.x:

import type {
  FieldValues,
  FormErrors,
  PartialValues,
  ValidateForm,
} from '@modular-forms/solid';
import type { ZodType } from 'zod';

/**
 * Creates a validation functions that parses the Zod schema of a form.
 *
 * @param schema A Zod schema.
 *
 * @returns A validation function.
 */
export function zodForm<TFieldValues extends FieldValues, TZod extends ZodType>(
  schema: TZod,
): ValidateForm<TFieldValues> {
  return async (values: PartialValues<TFieldValues>) => {
    const result = await schema.safeParseAsync(values);
    const formErrors: Record<string, string> = {};
    if (!result.success) {
      for (const issue of result.error.issues) {
        const path = issue.path.join('.');
        if (!formErrors[path]) {
          formErrors[path] = issue.message;
        }
      }
    }
    return formErrors as FormErrors<TFieldValues>;
  };
}

jmueller-ch avatar Oct 06 '25 18:10 jmueller-ch

Thank you for sharing!

fabian-hiller avatar Oct 07 '25 02:10 fabian-hiller

Although, looking at it again, while it works just fine, what it does not do is enforce that TZod has anything to do with TFieldValues like the previous version did.

This version solves that, but I suspect it doesn't work with Zod 3.

export function zodForm<
  TFieldValues extends FieldValues,
  TZod extends ZodType<TFieldValues>,
>(schema: TZod): ValidateForm<TFieldValues> {
  return async (values: PartialValues<TFieldValues>) => {
    const result = await schema.safeParseAsync(values);
    const formErrors: Record<string, string> = {};
    if (!result.success) {
      for (const issue of result.error.issues) {
        const path = issue.path.join('.');
        if (!formErrors[path]) {
          formErrors[path] = issue.message;
        }
      }
    }
    return formErrors as FormErrors<TFieldValues>;
  };
}

jmueller-ch avatar Oct 08 '25 20:10 jmueller-ch