Repository: react-hook-form/resolvers
Pull request #777 (2025-06-05 02:44:16)

feat: support Zod 4
Per some discussions I've been having with @bluebill1049, this PR adds support for Zod 4.

The other PR by @alexcraviotto (https://github.com/react-hook-form/resolvers/pull/776) is really solid but we decided to go another direction.

 
- This PR bumps the minimum Zod version to `3.25.0`
- With this change, it;s possible to support both Zod 3 and Zod 4 in a single `zodResolver` (via Zod's [subpath versioning](https://github.com/colinhacks/zod/issues/4371) scheme)
- I've designed this to continue working with Zod versions pre-3.25.0, but it's on a best-effort basis. For instance, you won't get perfect type signatures on parse params (the second argument to `zodResolver`). There were zero breaking changes between Zod 3.24 and Zod 3.25 so there's no reason not to upgrade.

Both Zod 3 and Zod 4 schemas can be passed into `zodResolver()`:

```ts
import { zodResolver } from "@hookform/resolvers";
import * as z3 from "zod/v3"; // or just "zod"
import * as z4 from "zod/v4";

const oldResolver = zodResolver(z3.object({ name: z.string() }));
const newResolver = zodResolver(z4.object({ name: z.string() }));
```


Jamess-Lucass: I've noticed the error messages returned from the form errors has changed when referencing zod/v4.

```typescript
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";

const formSchema = z.object({
  firstname: z.string().max(2),
});

type FormData = z.infer<typeof formSchema>;

function App() {
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting },
  } = useForm<FormData>({
    resolver: zodResolver(formSchema),
  });

  const onSubmit = (data: FormData) => {
    console.log("Form data:", data);
  };

  return (
    <div>
      <form onSubmit={handleSubmit(onSubmit)}>
        <label>First Name:</label>
        <input id="firstname" type="text" {...register("firstname")} />

        <button type="submit" disabled={isSubmitting}>
          Submit
        </button>
      </form>

      <p>{errors.firstname?.message}</p>
    </div>
  );
}

export default App;
```

The the input "longer_than_2_chars"

`errors.firstname?.message` would return "String must contain at most 2 character(s)", which i would expect.

when updating the import to `import { z } from "zod/v4";`

`errors.firstname?.message` now just returns "Invalid input". It doesn't seem to get the underlying zod error message anymore.


Patch:
diff --git a/README.md b/README.md
index 9ae0fd4..9fa6f4d 100644
--- a/README.md
+++ b/README.md
@@ -68,7 +68,7 @@ Example:
 ```tsx
 import { useForm } from 'react-hook-form';
 import { zodResolver } from '@hookform/resolvers/zod';
-import { z } from 'zod';
+import { z } from 'zod'; // or 'zod/v4'
 
 const schema = z.object({
   id: z.number(),
@@ -175,7 +175,7 @@ const App = () => {
 };
 ```
 
-### [Zod](https://github.com/vriad/zod)
+### [Zod](https://github.com/colinhacks/zod)
 
 TypeScript-first schema validation with static type inference
 
@@ -186,7 +186,7 @@ TypeScript-first schema validation with static type inference
 ```tsx
 import { useForm } from 'react-hook-form';
 import { zodResolver } from '@hookform/resolvers/zod';
-import { z } from 'zod';
+import { z } from 'zod'; // or 'zod/v4'
 
 const schema = z.object({
   name: z.string().min(1, { message: 'Required' }),
diff --git a/bun.lock b/bun.lock
index 529290a..6fbd1cf 100644
--- a/bun.lock
+++ b/bun.lock
@@ -54,7 +54,7 @@
         "vite-tsconfig-paths": "^5.1.4",
         "vitest": "^3.0.9",
         "yup": "^1.6.1",
-        "zod": "^3.24.2",
+        "zod": "^3.25.0",
       },
       "peerDependencies": {
         "react-hook-form": "^7.55.0",
@@ -1444,7 +1444,7 @@
 
     "yup": ["yup@1.6.1", "", { "dependencies": { "property-expr": "^2.0.5", "tiny-case": "^1.0.3", "toposort": "^2.0.2", "type-fest": "^2.19.0" } }, "sha512-JED8pB50qbA4FOkDol0bYF/p60qSEDQqBD0/qeIrUCG1KbPBIQ776fCUNb9ldbPcSTxA69g/47XTo4TqWiuXOA=="],
 
-    "zod": ["zod@3.24.2", "", {}, "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ=="],
+    "zod": ["zod@3.25.51", "", {}, "sha512-TQSnBldh+XSGL+opiSIq0575wvDPqu09AqWe1F7JhUMKY+M91/aGlK4MhpVNO7MgYfHcVCB1ffwAUTJzllKJqg=="],
 
     "@asamuzakjp/css-color/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
 
diff --git a/package.json b/package.json
index e6cfb9d..7319445 100644
--- a/package.json
+++ b/package.json
@@ -314,7 +314,7 @@
     "vite-tsconfig-paths": "^5.1.4",
     "vitest": "^3.0.9",
     "yup": "^1.6.1",
-    "zod": "^3.24.2"
+    "zod": "^3.25.0"
   },
   "peerDependencies": {
     "react-hook-form": "^7.55.0"
diff --git a/typeschema/src/typeschema.ts b/typeschema/src/typeschema.ts
index 0891a57..bba859b 100644
--- a/typeschema/src/typeschema.ts
+++ b/typeschema/src/typeschema.ts
@@ -1,4 +1,5 @@
 import { toNestErrors, validateFieldsNatively } from '@hookform/resolvers';
+import { StandardSchemaV1 } from '@standard-schema/spec';
 import {
   FieldError,
   FieldErrors,
@@ -6,7 +7,6 @@ import {
   Resolver,
   appendErrors,
 } from 'react-hook-form';
-import { StandardSchemaV1 } from 'zod/lib/standard-schema';
 
 const parseErrorSchema = (
   typeschemaErrors: readonly StandardSchemaV1.Issue[],
diff --git a/zod/src/zod.ts b/zod/src/zod.ts
index 064dcfd..0e227eb 100644
--- a/zod/src/zod.ts
+++ b/zod/src/zod.ts
@@ -8,13 +8,29 @@ import {
   ResolverSuccess,
   appendErrors,
 } from 'react-hook-form';
-import { ZodError, z } from 'zod';
+import * as z3 from 'zod/v3';
+import * as z4 from 'zod/v4/core';
 
-const isZodError = (error: any): error is ZodError =>
-  Array.isArray(error?.errors);
+const isZod3Error = (error: any): error is z3.ZodError => {
+  return Array.isArray(error?.issues);
+};
+const isZod3Schema = (schema: any): schema is z3.ZodSchema => {
+  return (
+    '_def' in schema &&
+    typeof schema._def === 'object' &&
+    'typeName' in schema._def
+  );
+};
+const isZod4Error = (error: any): error is z4.$ZodError => {
+  // instanceof is safe in Zod 4 (uses Symbol.hasInstance)
+  return error instanceof z4.$ZodError;
+};
+const isZod4Schema = (schema: any): schema is z4.$ZodType => {
+  return '_zod' in schema && typeof schema._zod === 'object';
+};
 
-function parseErrorSchema(
-  zodErrors: z.ZodIssue[],
+function parseZod3Issues(
+  zodErrors: z3.ZodIssue[],
   validateAllFieldCriteria: boolean,
 ) {
   const errors: Record<string, FieldError> = {};
@@ -63,37 +79,156 @@ function parseErrorSchema(
   return errors;
 }
 
+function parseZod4Issues(
+  zodErrors: z4.$ZodIssue[],
+  validateAllFieldCriteria: boolean,
+) {
+  const errors: Record<string, FieldError> = {};
+  // const _zodErrors = zodErrors as z4.$ZodISsue; //
+  for (; zodErrors.length; ) {
+    const error = zodErrors[0];
+    const { code, message, path } = error;
+    const _path = path.join('.');
+
+    if (!errors[_path]) {
+      if (error.code === 'invalid_union') {
+        const unionError = error.errors[0][0];
+
+        errors[_path] = {
+          message: unionError.message,
+          type: unionError.code,
+        };
+      } else {
+        errors[_path] = { message, type: code };
+      }
+    }
+
+    if (error.code === 'invalid_union') {
+      error.errors.forEach((unionError) =>
+        unionError.forEach((e) => zodErrors.push(e)),
+      );
+    }
+
+    if (validateAllFieldCriteria) {
+      const types = errors[_path].types;
+      const messages = types && types[error.code];
+
+      errors[_path] = appendErrors(
+        _path,
+        validateAllFieldCriteria,
+        errors,
+        code,
+        messages
+          ? ([] as string[]).concat(messages as string[], error.message)
+          : error.message,
+      ) as FieldError;
+    }
+
+    zodErrors.shift();
+  }
+
+  return errors;
+}
+
+type RawResolverOptions = {
+  mode?: 'async' | 'sync';
+  raw: true;
+};
+type NonRawResolverOptions = {
+  mode?: 'async' | 'sync';
+  raw?: false;
+};
+
+// minimal interfaces to avoid asssignability issues between versions
+interface Zod3Type<O = unknown, I = unknown> {
+  _output: O;
+  _input: I;
+  _def: {
+    typeName: string;
+  };
+}
+
+// some type magic to make versions pre-3.25.0 still work
+type IsUnresolved<T> = PropertyKey extends keyof T ? true : false;
+type UnresolvedFallback<T, Fallback> = IsUnresolved<typeof z3> extends true
+  ? Fallback
+  : T;
+type FallbackIssue = {
+  code: string;
+  message: string;
+  path: (string | number)[];
+};
+type Zod3ParseParams = UnresolvedFallback<
+  z3.ParseParams,
+  // fallback if user is on <3.25.0
+  {
+    path?: (string | number)[];
+    errorMap?: (
+      iss: FallbackIssue,
+      ctx: {
+        defaultError: string;
+        data: any;
+      },
+    ) => { message: string };
+    async?: boolean;
+  }
+>;
+type Zod4ParseParams = UnresolvedFallback<
+  z4.ParseContext<z4.$ZodIssue>,
+  // fallback if user is on <3.25.0
+  {
+    readonly error?: (
+      iss: FallbackIssue,
+    ) => null | undefined | string | { message: string };
+    readonly reportInput?: boolean;
+    readonly jitless?: boolean;
+  }
+>;
+
 export function zodResolver<Input extends FieldValues, Context, Output>(
-  schema: z.ZodSchema<Output, any, Input>,
-  schemaOptions?: Partial<z.ParseParams>,
-  resolverOptions?: {
-    mode?: 'async' | 'sync';
-    raw?: false;
-  },
+  schema: Zod3Type<Output, Input>,
+  schemaOptions?: Zod3ParseParams,
+  resolverOptions?: NonRawResolverOptions,
 ): Resolver<Input, Context, Output>;
-
 export function zodResolver<Input extends FieldValues, Context, Output>(
-  schema: z.ZodSchema<Output, any, Input>,
-  schemaOptions: Partial<z.ParseParams> | undefined,
-  resolverOptions: {
-    mode?: 'async' | 'sync';
-    raw: true;
-  },
+  schema: Zod3Type<Output, Input>,
+  schemaOptions: Zod3ParseParams | undefined,
+  resolverOptions: RawResolverOptions,
 ): Resolver<Input, Context, Input>;
-
+// the Zod 4 overloads need to be generic for complicated reasons
+export function zodResolver<
+  Input extends FieldValues,
+  Context,
+  Output,
+  T extends z4.$ZodType<Output, Input> = z4.$ZodType<Output, Input>,
+>(
+  schema: T,
+  schemaOptions?: Zod4ParseParams, // already partial
+  resolverOptions?: NonRawResolverOptions,
+): Resolver<z4.input<T>, Context, z4.output<T>>;
+export function zodResolver<
+  Input extends FieldValues,
+  Context,
+  Output,
+  T extends z4.$ZodType<Output, Input> = z4.$ZodType<Output, Input>,
+>(
+  schema: z4.$ZodType<Output, Input>,
+  schemaOptions: Zod4ParseParams | undefined, // already partial
+  resolverOptions: RawResolverOptions,
+): Resolver<z4.input<T>, Context, z4.input<T>>;
 /**
  * Creates a resolver function for react-hook-form that validates form data using a Zod schema
- * @param {z.ZodSchema<Input>} schema - The Zod schema used to validate the form data
- * @param {Partial<z.ParseParams>} [schemaOptions] - Optional configuration options for Zod parsing
+ * @param {z3.ZodSchema<Input>} schema - The Zod schema used to validate the form data
+ * @param {Partial<z3.ParseParams>} [schemaOptions] - Optional configuration options for Zod parsing
  * @param {Object} [resolverOptions] - Optional resolver-specific configuration
  * @param {('async'|'sync')} [resolverOptions.mode='async'] - Validation mode. Use 'sync' for synchronous validation
  * @param {boolean} [resolverOptions.raw=false] - If true, returns the raw form values instead of the parsed data
- * @returns {Resolver<z.output<typeof schema>>} A resolver function compatible with react-hook-form
+ * @returns {Resolver<z3.output<typeof schema>>} A resolver function compatible with react-hook-form
  * @throws {Error} Throws if validation fails with a non-Zod error
  * @example
- * const schema = z.object({
- *   name: z.string().min(2),
- *   age: z.number().min(18)
+ * const schema = z3.object({
+ *   name: z3.string().min(2),
+ *   age: z3.number().min(18)
  * });
  *
  * useForm({
@@ -101,41 +236,80 @@ export function zodResolver<Input extends FieldValues, Context, Output>(
  * });
  */
 export function zodResolver<Input extends FieldValues, Context, Output>(
-  schema: z.ZodSchema<Output, any, Input>,
-  schemaOptions?: Partial<z.ParseParams>,
+  schema: object,
+  schemaOptions?: object,
   resolverOptions: {
     mode?: 'async' | 'sync';
     raw?: boolean;
   } = {},
 ): Resolver<Input, Context, Output | Input> {
-  return async (values: Input, _, options) => {
-    try {
-      const data = await schema[
-        resolverOptions.mode === 'sync' ? 'parse' : 'parseAsync'
-      ](values, schemaOptions);
-
-      options.shouldUseNativeValidation && validateFieldsNatively({}, options);
-
-      return {
-        errors: {} as FieldErrors,
-        values: resolverOptions.raw ? Object.assign({}, values) : data,
-      } satisfies ResolverSuccess<Output | Input>;
-    } catch (error) {
-      if (isZodError(error)) {
+  if (isZod3Schema(schema)) {
+    return async (values: Input, _, options) => {
+      try {
+        const data = await schema[
+          resolverOptions.mode === 'sync' ? 'parse' : 'parseAsync'
+        ](values, schemaOptions);
+
+        options.shouldUseNativeValidation &&
+          validateFieldsNatively({}, options);
+
         return {
-          values: {},
-          errors: toNestErrors(
-            parseErrorSchema(
-              error.errors,
-              !options.shouldUseNativeValidation &&
-                options.criteriaMode === 'all',
+          errors: {} as FieldErrors,
+          values: resolverOptions.raw ? Object.assign({}, values) : data,
+        } satisfies ResolverSuccess<Output | Input>;
+      } catch (error) {
+        if (isZod3Error(error)) {
+          return {
+            values: {},
+            errors: toNestErrors(
+              parseZod3Issues(
+                error.errors,
+                !options.shouldUseNativeValidation &&
+                  options.criteriaMode === 'all',
+              ),
+              options,
             ),
-            options,
-          ),
-        } satisfies ResolverError<Input>;
+          } satisfies ResolverError<Input>;
+        }
+
+        throw error;
       }
+    };
+  }
 
-      throw error;
-    }
-  };
+  if (isZod4Schema(schema)) {
+    return async (values: Input, _, options) => {
+      try {
+        const parseFn =
+          resolverOptions.mode === 'sync' ? z4.parse : z4.parseAsync;
+        const data: any = await parseFn(schema, values, schemaOptions);
+
+        options.shouldUseNativeValidation &&
+          validateFieldsNatively({}, options);
+
+        return {
+          errors: {} as FieldErrors,
+          values: resolverOptions.raw ? Object.assign({}, values) : data,
+        } satisfies ResolverSuccess<Output | Input>;
+      } catch (error) {
+        if (isZod4Error(error)) {
+          return {
+            values: {},
+            errors: toNestErrors(
+              parseZod4Issues(
+                error.issues,
+                !options.shouldUseNativeValidation &&
+                  options.criteriaMode === 'all',
+              ),
+              options,
+            ),
+          } satisfies ResolverError<Input>;
+        }
+
+        throw error;
+      }
+    };
+  }
+
+  throw new Error('Invalid input: not a Zod schema');
 }


Test patch:
diff --git a/standard-schema/src/__tests__/__fixtures__/data.ts b/standard-schema/src/__tests__/__fixtures__/data.ts
index fc72250..7236799 100644
--- a/standard-schema/src/__tests__/__fixtures__/data.ts
+++ b/standard-schema/src/__tests__/__fixtures__/data.ts
@@ -1,6 +1,6 @@
 import { StandardSchemaV1 } from '@standard-schema/spec';
 import { Field, InternalFieldName } from 'react-hook-form';
-import { z } from 'zod';
+import { z } from 'zod/v3';
 
 export const schema = z
   .object({
diff --git a/standard-schema/src/__tests__/standard-schema.ts b/standard-schema/src/__tests__/standard-schema.ts
index b5c5391..b3c319d 100644
--- a/standard-schema/src/__tests__/standard-schema.ts
+++ b/standard-schema/src/__tests__/standard-schema.ts
@@ -1,5 +1,5 @@
 import { Resolver, SubmitHandler, useForm } from 'react-hook-form';
-import { z } from 'zod';
+import { z } from 'zod/v3';
 import { standardSchemaResolver } from '..';
 import {
   customSchema,
diff --git a/typebox/src/__tests__/typebox.ts b/typebox/src/__tests__/typebox.ts
index 0085949..7fb7dfa 100644
--- a/typebox/src/__tests__/typebox.ts
+++ b/typebox/src/__tests__/typebox.ts
@@ -1,8 +1,8 @@
+import { Type } from '@sinclair/typebox';
+import { TypeCompiler } from '@sinclair/typebox/compiler';
 import { Resolver, SubmitHandler, useForm } from 'react-hook-form';
 import { typeboxResolver } from '..';
 import { fields, invalidData, schema, validData } from './__fixtures__/data';
-import { Type } from '@sinclair/typebox';
-import { TypeCompiler } from '@sinclair/typebox/compiler';
 
 const shouldUseNativeValidation = false;
 
diff --git a/typeschema/src/__tests__/Form-native-validation.tsx b/typeschema/src/__tests__/Form-native-validation.tsx
index a1b8732..55c70bb 100644
--- a/typeschema/src/__tests__/Form-native-validation.tsx
+++ b/typeschema/src/__tests__/Form-native-validation.tsx
@@ -3,7 +3,7 @@ import user from '@testing-library/user-event';
 import type { Infer } from '@typeschema/main';
 import React from 'react';
 import { useForm } from 'react-hook-form';
-import { z } from 'zod';
+import { z } from 'zod/v3';
 import { typeschemaResolver } from '..';
 
 const USERNAME_REQUIRED_MESSAGE = 'username field is required';
diff --git a/typeschema/src/__tests__/Form.tsx b/typeschema/src/__tests__/Form.tsx
index 173e799..51312a6 100644
--- a/typeschema/src/__tests__/Form.tsx
+++ b/typeschema/src/__tests__/Form.tsx
@@ -3,7 +3,7 @@ import user from '@testing-library/user-event';
 import type { Infer } from '@typeschema/main';
 import React from 'react';
 import { useForm } from 'react-hook-form';
-import { z } from 'zod';
+import { z } from 'zod/v3';
 import { typeschemaResolver } from '..';
 
 const schema = z.object({
diff --git a/typeschema/src/__tests__/__fixtures__/data.ts b/typeschema/src/__tests__/__fixtures__/data.ts
index 3131067..d206eee 100644
--- a/typeschema/src/__tests__/__fixtures__/data.ts
+++ b/typeschema/src/__tests__/__fixtures__/data.ts
@@ -1,5 +1,5 @@
 import { Field, InternalFieldName } from 'react-hook-form';
-import { z } from 'zod';
+import { z } from 'zod/v3';
 
 export const schema = z
   .object({
diff --git a/typeschema/src/__tests__/typeschema.ts b/typeschema/src/__tests__/typeschema.ts
index 5261fbd..a60ab07 100644
--- a/typeschema/src/__tests__/typeschema.ts
+++ b/typeschema/src/__tests__/typeschema.ts
@@ -1,6 +1,6 @@
 import * as typeschema from '@typeschema/main';
 import { Resolver, SubmitHandler, useForm } from 'react-hook-form';
-import { z } from 'zod';
+import { z } from 'zod/v3';
 import { typeschemaResolver } from '..';
 import { fields, invalidData, schema, validData } from './__fixtures__/data';
 
diff --git a/zod/src/__tests__/Form-native-validation.tsx b/zod/src/__tests__/Form-native-validation.tsx
index 3ac296f..68957de 100644
--- a/zod/src/__tests__/Form-native-validation.tsx
+++ b/zod/src/__tests__/Form-native-validation.tsx
@@ -2,7 +2,7 @@ import { render, screen } from '@testing-library/react';
 import user from '@testing-library/user-event';
 import React from 'react';
 import { useForm } from 'react-hook-form';
-import { z } from 'zod';
+import { z } from 'zod/v3';
 import { zodResolver } from '..';
 
 const USERNAME_REQUIRED_MESSAGE = 'username field is required';
diff --git a/zod/src/__tests__/Form.tsx b/zod/src/__tests__/Form.tsx
index ad78b48..d766924 100644
--- a/zod/src/__tests__/Form.tsx
+++ b/zod/src/__tests__/Form.tsx
@@ -2,7 +2,7 @@ import { render, screen } from '@testing-library/react';
 import user from '@testing-library/user-event';
 import React from 'react';
 import { useForm } from 'react-hook-form';
-import { z } from 'zod';
+import { z } from 'zod/v3';
 import { zodResolver } from '..';
 
 const schema = z.object({
diff --git a/zod/src/__tests__/__fixtures__/data.ts b/zod/src/__tests__/__fixtures__/data-v3.ts
similarity index 98%
rename from zod/src/__tests__/__fixtures__/data.ts
rename to zod/src/__tests__/__fixtures__/data-v3.ts
index f338f1d..4904ce2 100644
--- a/zod/src/__tests__/__fixtures__/data.ts
+++ b/zod/src/__tests__/__fixtures__/data-v3.ts
@@ -1,5 +1,5 @@
 import { Field, InternalFieldName } from 'react-hook-form';
-import { z } from 'zod';
+import { z } from 'zod/v3';
 
 export const schema = z
   .object({
diff --git a/zod/src/__tests__/__fixtures__/data-v4-mini.ts b/zod/src/__tests__/__fixtures__/data-v4-mini.ts
new file mode 100644
index 0000000..11bab77
--- /dev/null
+++ b/zod/src/__tests__/__fixtures__/data-v4-mini.ts
@@ -0,0 +1,98 @@
+import { Field, InternalFieldName } from 'react-hook-form';
+import { z } from 'zod/v4-mini';
+
+export const schema = z
+  .object({
+    username: z
+      .string()
+      .check(z.regex(/^\w+$/), z.minLength(3), z.maxLength(30)),
+    password: z
+      .string()
+      .check(
+        z.regex(new RegExp('.*[A-Z].*'), 'One uppercase character'),
+        z.regex(new RegExp('.*[a-z].*'), 'One lowercase character'),
+        z.regex(new RegExp('.*\\d.*'), 'One number'),
+        z.regex(
+          new RegExp('.*[`~<>?,./!@#$%^&*()\\-_+="\'|{}\\[\\];:\\\\].*'),
+          'One special character',
+        ),
+        z.minLength(8, 'Must be at least 8 characters in length'),
+      ),
+    repeatPassword: z.string(),
+    accessToken: z.union([z.string(), z.number()]),
+    birthYear: z.optional(z.number().check(z.minimum(1900), z.maximum(2013))),
+    email: z.optional(z.email()),
+    tags: z.array(z.string()),
+    enabled: z.boolean(),
+    url: z.union([z.url('Custom error url'), z.literal('')]),
+    like: z.optional(
+      z.array(
+        z.object({
+          id: z.number(),
+          name: z.string().check(z.length(4)),
+        }),
+      ),
+    ),
+    dateStr: z
+      .pipe(
+        z.string(),
+        z.transform((value) => new Date(value)),
+      )
+      .check(
+        z.refine((value) => !isNaN(value.getTime()), {
+          message: 'Invalid date',
+        }),
+      ),
+  })
+  .check(
+    z.refine((obj) => obj.password === obj.repeatPassword, {
+      message: 'Passwords do not match',
+      path: ['confirm'],
+    }),
+  );
+
+export const validData = {
+  username: 'Doe',
+  password: 'Password123_',
+  repeatPassword: 'Password123_',
+  birthYear: 2000,
+  email: 'john@doe.com',
+  tags: ['tag1', 'tag2'],
+  enabled: true,
+  accessToken: 'accessToken',
+  url: 'https://react-hook-form.com/',
+  like: [
+    {
+      id: 1,
+      name: 'name',
+    },
+  ],
+  dateStr: '2020-01-01',
+} satisfies z.input<typeof schema>;
+
+export const invalidData = {
+  password: '___',
+  email: '',
+  birthYear: 'birthYear',
+  like: [{ id: 'z' }],
+  url: 'abc',
+} as unknown as z.input<typeof schema>;
+
+export const fields: Record<InternalFieldName, Field['_f']> = {
+  username: {
+    ref: { name: 'username' },
+    name: 'username',
+  },
+  password: {
+    ref: { name: 'password' },
+    name: 'password',
+  },
+  email: {
+    ref: { name: 'email' },
+    name: 'email',
+  },
+  birthday: {
+    ref: { name: 'birthday' },
+    name: 'birthday',
+  },
+};
diff --git a/zod/src/__tests__/__fixtures__/data-v4.ts b/zod/src/__tests__/__fixtures__/data-v4.ts
new file mode 100644
index 0000000..7c6f55f
--- /dev/null
+++ b/zod/src/__tests__/__fixtures__/data-v4.ts
@@ -0,0 +1,89 @@
+import { Field, InternalFieldName } from 'react-hook-form';
+import { z } from 'zod/v4';
+
+export const schema = z
+  .object({
+    username: z.string().regex(/^\w+$/).min(3).max(30),
+    password: z
+      .string()
+      .regex(new RegExp('.*[A-Z].*'), 'One uppercase character')
+      .regex(new RegExp('.*[a-z].*'), 'One lowercase character')
+      .regex(new RegExp('.*\\d.*'), 'One number')
+      .regex(
+        new RegExp('.*[`~<>?,./!@#$%^&*()\\-_+="\'|{}\\[\\];:\\\\].*'),
+        'One special character',
+      )
+      .min(8, 'Must be at least 8 characters in length'),
+    repeatPassword: z.string(),
+    accessToken: z.union([z.string(), z.number()]),
+    birthYear: z.number().min(1900).max(2013).optional(),
+    email: z.string().email().optional(),
+    tags: z.array(z.string()),
+
+    enabled: z.boolean(),
+    url: z.string().url('Custom error url').or(z.literal('')),
+    like: z
+      .array(
+        z.object({
+          id: z.number(),
+          name: z.string().length(4),
+        }),
+      )
+      .optional(),
+    dateStr: z
+      .string()
+      .transform((value) => new Date(value))
+      .refine((value) => !isNaN(value.getTime()), {
+        message: 'Invalid date',
+      }),
+  })
+  .refine((obj) => obj.password === obj.repeatPassword, {
+    message: 'Passwords do not match',
+    path: ['confirm'],
+  });
+
+export const validData = {
+  username: 'Doe',
+  password: 'Password123_',
+  repeatPassword: 'Password123_',
+  birthYear: 2000,
+  email: 'john@doe.com',
+  tags: ['tag1', 'tag2'],
+  enabled: true,
+  accessToken: 'accessToken',
+  url: 'https://react-hook-form.com/',
+  like: [
+    {
+      id: 1,
+      name: 'name',
+    },
+  ],
+  dateStr: '2020-01-01',
+} satisfies z.input<typeof schema>;
+
+export const invalidData = {
+  password: '___',
+  email: '',
+  birthYear: 'birthYear',
+  like: [{ id: 'z' }],
+  url: 'abc',
+} as unknown as z.input<typeof schema>;
+
+export const fields: Record<InternalFieldName, Field['_f']> = {
+  username: {
+    ref: { name: 'username' },
+    name: 'username',
+  },
+  password: {
+    ref: { name: 'password' },
+    name: 'password',
+  },
+  email: {
+    ref: { name: 'email' },
+    name: 'email',
+  },
+  birthday: {
+    ref: { name: 'birthday' },
+    name: 'birthday',
+  },
+};
diff --git a/zod/src/__tests__/zod-v3.ts b/zod/src/__tests__/zod-v3.ts
new file mode 100644
index 0000000..8e040ba
--- /dev/null
+++ b/zod/src/__tests__/zod-v3.ts
@@ -0,0 +1,178 @@
+import { Resolver, SubmitHandler, useForm } from 'react-hook-form';
+import { z } from 'zod/v3';
+import { zodResolver } from '..';
+import { fields, invalidData, schema, validData } from './__fixtures__/data-v3';
+
+const shouldUseNativeValidation = false;
+
+describe('zodResolver', () => {
+  it('should return values from zodResolver when validation pass & raw=true', async () => {
+    const parseAsyncSpy = vi.spyOn(schema, 'parseAsync');
+
+    const result = await zodResolver(schema, undefined, {
+      raw: true,
+    })(validData, undefined, {
+      fields,
+      shouldUseNativeValidation,
+    });
+
+    expect(parseAsyncSpy).toHaveBeenCalledTimes(1);
+    expect(result).toEqual({ errors: {}, values: validData });
+  });
+
+  it('should return parsed values from zodResolver with `mode: sync` when validation pass', async () => {
+    const parseSpy = vi.spyOn(schema, 'parse');
+    const parseAsyncSpy = vi.spyOn(schema, 'parseAsync');
+
+    const result = await zodResolver(schema, undefined, {
+      mode: 'sync',
+    })(validData, undefined, { fields, shouldUseNativeValidation });
+
+    expect(parseSpy).toHaveBeenCalledTimes(1);
+    expect(parseAsyncSpy).not.toHaveBeenCalled();
+    expect(result.errors).toEqual({});
+    expect(result).toMatchSnapshot();
+  });
+
+  it('should return a single error from zodResolver when validation fails', async () => {
+    const result = await zodResolver(schema)(invalidData, undefined, {
+      fields,
+      shouldUseNativeValidation,
+    });
+
+    expect(result).toMatchSnapshot();
+  });
+
+  it('should return a single error from zodResolver with `mode: sync` when validation fails', async () => {
+    const parseSpy = vi.spyOn(schema, 'parse');
+    const parseAsyncSpy = vi.spyOn(schema, 'parseAsync');
+
+    const result = await zodResolver(schema, undefined, {
+      mode: 'sync',
+    })(invalidData, undefined, { fields, shouldUseNativeValidation });
+
+    expect(parseSpy).toHaveBeenCalledTimes(1);
+    expect(parseAsyncSpy).not.toHaveBeenCalled();
+    expect(result).toMatchSnapshot();
+  });
+
+  it('should return all the errors from zodResolver when validation fails with `validateAllFieldCriteria` set to true', async () => {
+    const result = await zodResolver(schema)(invalidData, undefined, {
+      fields,
+      criteriaMode: 'all',
+      shouldUseNativeValidation,
+    });
+
+    expect(result).toMatchSnapshot();
+  });
+
+  it('should return all the errors from zodResolver when validation fails with `validateAllFieldCriteria` set to true and `mode: sync`', async () => {
+    const result = await zodResolver(schema, undefined, { mode: 'sync' })(
+      invalidData,
+      undefined,
+      {
+        fields,
+        criteriaMode: 'all',
+        shouldUseNativeValidation,
+      },
+    );
+
+    expect(result).toMatchSnapshot();
+  });
+
+  it('should throw any error unrelated to Zod', async () => {
+    const schemaWithCustomError = schema.refine(() => {
+      throw Error('custom error');
+    });
+    const promise = zodResolver(schemaWithCustomError)(validData, undefined, {
+      fields,
+      shouldUseNativeValidation,
+    });
+
+    await expect(promise).rejects.toThrow('custom error');
+  });
+
+  it('should enforce parse params type signature', async () => {
+    const resolver = zodResolver(schema, {
+      async: true,
+      path: ['asdf', 1234],
+      errorMap(iss, ctx) {
+        iss.path;
+        iss.code;
+        iss.path;
+        ctx.data;
+        ctx.defaultError;
+        return { message: 'asdf' };
+      },
+    });
+
+    resolver;
+  });
+
+  /**
+   * Type inference tests
+   */
+  it('should correctly infer the output type from a zod schema', () => {
+    const resolver = zodResolver(z.object({ id: z.number() }));
+
+    expectTypeOf(resolver).toEqualTypeOf<
+      Resolver<{ id: number }, unknown, { id: number }>
+    >();
+  });
+
+  it('should correctly infer the output type from a zod schema using a transform', () => {
+    const resolver = zodResolver(
+      z.object({ id: z.number().transform((val) => String(val)) }),
+    );
+
+    expectTypeOf(resolver).toEqualTypeOf<
+      Resolver<{ id: number }, unknown, { id: string }>
+    >();
+  });
+
+  it('should correctly infer the output type from a zod schema when a different input type is specified', () => {
+    const schema = z.object({ id: z.number() }).transform(({ id }) => {
+      return { id: String(id) };
+    });
+
+    const resolver = zodResolver<{ id: number }, any, z.output<typeof schema>>(
+      schema,
+    );
+
+    expectTypeOf(resolver).toEqualTypeOf<
+      Resolver<{ id: number }, any, { id: string }>
+    >();
+  });
+
+  it('should correctly infer the output type from a Zod schema for the handleSubmit function in useForm', () => {
+    const schema = z.object({ id: z.number() });
+
+    const form = useForm({
+      resolver: zodResolver(schema),
+    });
+
+    expectTypeOf(form.watch('id')).toEqualTypeOf<number>();
+
+    expectTypeOf(form.handleSubmit).parameter(0).toEqualTypeOf<
+      SubmitHandler<{
+        id: number;
+      }>
+    >();
+  });
+
+  it('should correctly infer the output type from a Zod schema with a transform for the handleSubmit function in useForm', () => {
+    const schema = z.object({ id: z.number().transform((val) => String(val)) });
+
+    const form = useForm({
+      resolver: zodResolver(schema),
+    });
+
+    expectTypeOf(form.watch('id')).toEqualTypeOf<number>();
+
+    expectTypeOf(form.handleSubmit).parameter(0).toEqualTypeOf<
+      SubmitHandler<{
+        id: string;
+      }>
+    >();
+  });
+});
diff --git a/zod/src/__tests__/zod-v4-mini.ts b/zod/src/__tests__/zod-v4-mini.ts
new file mode 100644
index 0000000..54c5681
--- /dev/null
+++ b/zod/src/__tests__/zod-v4-mini.ts
@@ -0,0 +1,182 @@
+import { Resolver, SubmitHandler, useForm } from 'react-hook-form';
+import { z } from 'zod/v4-mini';
+import { zodResolver } from '..';
+import {
+  fields,
+  invalidData,
+  schema,
+  validData,
+} from './__fixtures__/data-v4-mini';
+
+const shouldUseNativeValidation = false;
+
+describe('zodResolver', () => {
+  it('should return values from zodResolver when validation pass & raw=true', async () => {
+    const parseAsyncSpy = vi.spyOn(schema, 'parseAsync');
+
+    const result = await zodResolver(schema, undefined, {
+      raw: true,
+    })(validData, undefined, {
+      fields,
+      shouldUseNativeValidation,
+    });
+    result;
+
+    expect(parseAsyncSpy).toHaveBeenCalledTimes(1);
+    expect(result).toEqual({ errors: {}, values: validData });
+    expectTypeOf(result.values);
+  });
+
+  it('should return parsed values from zodResolver with `mode: sync` when validation pass', async () => {
+    const parseSpy = vi.spyOn(schema, 'parse');
+    const parseAsyncSpy = vi.spyOn(schema, 'parseAsync');
+
+    const result = await zodResolver(schema, undefined, {
+      mode: 'sync',
+    })(validData, undefined, { fields, shouldUseNativeValidation });
+    expect(parseSpy).toHaveBeenCalledTimes(1);
+    expect(parseAsyncSpy).not.toHaveBeenCalled();
+    expect(result.errors).toEqual({});
+    expect(result).toMatchSnapshot();
+  });
+
+  it('should return a single error from zodResolver when validation fails', async () => {
+    const result = await zodResolver(schema)(invalidData, undefined, {
+      fields,
+      shouldUseNativeValidation,
+    });
+
+    expect(result).toMatchSnapshot();
+  });
+
+  it('should return a single error from zodResolver with `mode: sync` when validation fails', async () => {
+    const parseSpy = vi.spyOn(schema, 'parse');
+    const parseAsyncSpy = vi.spyOn(schema, 'parseAsync');
+
+    const result = await zodResolver(schema, undefined, {
+      mode: 'sync',
+    })(invalidData, undefined, { fields, shouldUseNativeValidation });
+
+    expect(parseSpy).toHaveBeenCalledTimes(1);
+    expect(parseAsyncSpy).not.toHaveBeenCalled();
+    expect(result).toMatchSnapshot();
+  });
+
+  it('should return all the errors from zodResolver when validation fails with `validateAllFieldCriteria` set to true', async () => {
+    const result = await zodResolver(schema)(invalidData, undefined, {
+      fields,
+      criteriaMode: 'all',
+      shouldUseNativeValidation,
+    });
+
+    expect(result).toMatchSnapshot();
+  });
+
+  it('should return all the errors from zodResolver when validation fails with `validateAllFieldCriteria` set to true and `mode: sync`', async () => {
+    const result = await zodResolver(schema, undefined, { mode: 'sync' })(
+      invalidData,
+      undefined,
+      {
+        fields,
+        criteriaMode: 'all',
+        shouldUseNativeValidation,
+      },
+    );
+
+    expect(result).toMatchSnapshot();
+  });
+
+  it('should throw any error unrelated to Zod', async () => {
+    const schemaWithCustomError = schema.check(
+      z.refine(() => {
+        throw Error('custom error');
+      }),
+    );
+    const promise = zodResolver(schemaWithCustomError)(validData, undefined, {
+      fields,
+      shouldUseNativeValidation,
+    });
+
+    await expect(promise).rejects.toThrow('custom error');
+  });
+
+  /**
+   * Type inference tests
+   */
+  it('should correctly infer the output type from a zod schema', () => {
+    const resolver = zodResolver(z.object({ id: z.number() }));
+
+    expectTypeOf(resolver).toEqualTypeOf<
+      Resolver<{ id: number }, unknown, { id: number }>
+    >();
+  });
+
+  it('should correctly infer the output type from a zod schema using a transform', () => {
+    const resolver = zodResolver(
+      z.object({
+        id: z.pipe(
+          z.number(),
+          z.transform((val) => String(val)),
+        ),
+      }),
+    );
+
+    expectTypeOf(resolver).toEqualTypeOf<
+      Resolver<{ id: number }, unknown, { id: string }>
+    >();
+  });
+
+  it('should correctly infer the output type from a zod schema when a different input type is specified', () => {
+    const schema = z.pipe(
+      z.object({ id: z.number() }),
+      z.transform(({ id }) => {
+        return { id: String(id) };
+      }),
+    );
+
+    const resolver = zodResolver<{ id: number }, any, z.output<typeof schema>>(
+      schema,
+    );
+
+    expectTypeOf(resolver).toEqualTypeOf<
+      Resolver<{ id: number }, any, { id: string }>
+    >();
+  });
+
+  it('should correctly infer the output type from a Zod schema for the handleSubmit function in useForm', () => {
+    const schema = z.object({ id: z.number() });
+
+    const form = useForm({
+      resolver: zodResolver(schema),
+    });
+
+    expectTypeOf(form.watch('id')).toEqualTypeOf<number>();
+
+    expectTypeOf(form.handleSubmit).parameter(0).toEqualTypeOf<
+      SubmitHandler<{
+        id: number;
+      }>
+    >();
+  });
+
+  it('should correctly infer the output type from a Zod schema with a transform for the handleSubmit function in useForm', () => {
+    const schema = z.object({
+      id: z.pipe(
+        z.number(),
+        z.transform((val) => String(val)),
+      ),
+    });
+
+    const form = useForm({
+      resolver: zodResolver(schema),
+    });
+
+    expectTypeOf(form.watch('id')).toEqualTypeOf<number>();
+
+    expectTypeOf(form.handleSubmit).parameter(0).toEqualTypeOf<
+      SubmitHandler<{
+        id: string;
+      }>
+    >();
+  });
+});
diff --git a/zod/src/__tests__/zod.ts b/zod/src/__tests__/zod-v4.ts
similarity index 93%
rename from zod/src/__tests__/zod.ts
rename to zod/src/__tests__/zod-v4.ts
index 4b2f75b..0a043bc 100644
--- a/zod/src/__tests__/zod.ts
+++ b/zod/src/__tests__/zod-v4.ts
@@ -1,7 +1,7 @@
 import { Resolver, SubmitHandler, useForm } from 'react-hook-form';
-import { z } from 'zod';
+import { z } from 'zod/v4';
 import { zodResolver } from '..';
-import { fields, invalidData, schema, validData } from './__fixtures__/data';
+import { fields, invalidData, schema, validData } from './__fixtures__/data-v4';
 
 const shouldUseNativeValidation = false;
 
@@ -92,6 +92,21 @@ describe('zodResolver', () => {
     await expect(promise).rejects.toThrow('custom error');
   });
 
+  it('should enforce parse params type signature', async () => {
+    const resolver = zodResolver(schema, {
+      jitless: true,
+      reportInput: true,
+      error(iss) {
+        iss.path;
+        iss.code;
+        iss.path;
+        return { message: 'asdf' };
+      },
+    });
+
+    resolver;
+  });
+
   /**
    * Type inference tests
    */