npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@zod-utils/react-hook-form

v7.1.0

Published

React Hook Form integration and utilities for Zod schemas

Readme

@zod-utils/react-hook-form

npm version npm downloads Bundle Size License: MIT TypeScript CI codecov

React Hook Form integration and utilities for Zod schemas.

💡 Why Use This?

The whole point: Automatically transforms your Zod schema types so form inputs accept undefined (and null for objects only) during editing, while the validated output remains exactly as your Zod schema defines.

No more type wrestling with React Hook Form - just pass your schema and it works.

import { useZodForm } from "@zod-utils/react-hook-form";
import { z } from "zod";

// Your schema with primitives, arrays, and objects - NOT optional
const schema = z.object({
  username: z.string().min(3),
  age: z.number().min(18),
  tags: z.array(z.string()),
  profile: z.object({ bio: z.string() }),
});

const form = useZodForm({ schema });

// ✅ Works! Primitives and arrays accept undefined during editing
form.setValue("username", undefined);
form.setValue("age", undefined);
form.setValue("tags", undefined);

// ✅ Works! Objects accept both null and undefined
form.setValue("profile", null);
form.setValue("profile", undefined);

// ✅ Validated output type is exactly z.infer<typeof schema>
const onSubmit = form.handleSubmit((data) => {
  // Type: { username: string; age: number; tags: string[]; profile: { bio: string } }
  // NOT { username: string | null | undefined; ... }
  console.log(data.username); // Type: string
  console.log(data.age); // Type: number
  console.log(data.tags); // Type: string[]
  console.log(data.profile); // Type: { bio: string }
});

Installation

npm install @zod-utils/react-hook-form zod react react-hook-form @hookform/resolvers

Related Packages

  • @zod-utils/core - Pure TypeScript utilities for Zod schema manipulation (no React dependencies). All utilities are re-exported from this package for convenience.

Features

  • 🎣 useZodForm - Automatic type transformation for form inputs (nullable/undefined) while preserving Zod schema validation
  • 📋 FormSchemaProvider - React Context for providing schema to form components
  • useIsRequiredField - Hook to check if a field requires valid input
  • 🔄 Discriminated Union Support - Full type-safe support for discriminated unions
  • 📦 All core utilities - Re-exports everything from @zod-utils/core
  • ⚛️ React-optimized - Built specifically for React applications

Quick Start

import { useZodForm, getSchemaDefaults } from "@zod-utils/react-hook-form";
import { z } from "zod";

const schema = z.object({
  name: z.string().default("John Doe"),
  email: z.string().email(),
  age: z.number().min(18),
});

function MyForm() {
  const form = useZodForm({
    schema,
    defaultValues: getSchemaDefaults(schema),
  });

  const onSubmit = form.handleSubmit((data) => {
    console.log(data);
  });

  return (
    <form onSubmit={onSubmit}>
      <input {...form.register("name")} />
      <input {...form.register("email")} type="email" />
      <input {...form.register("age")} type="number" />
      <button type="submit">Submit</button>
    </form>
  );
}

API Reference

useZodForm(config)

Type-safe wrapper around React Hook Form's useForm with automatic Zod schema integration.

import { useZodForm } from "@zod-utils/react-hook-form";
import { z } from "zod";

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

const form = useZodForm({
  schema, // Zod schema (required)
  defaultValues: {
    /* ... */
  }, // Optional default values
  zodResolverOptions: {
    /* ... */
  }, // Optional zodResolver options
  // ... all other useForm options
});

What it does:

  • Input transformation (by default):
    • Primitive fields (string, number, boolean) accept undefined only
    • Array fields accept undefined only
    • Object fields accept both null and undefined
    • You can override this by specifying a custom input type (see examples below)
  • Output validation: Validated data matches your Zod schema exactly
  • Type inference: No manual type annotations needed - everything is inferred from the schema
  • Zod integration: Automatically sets up zodResolver for validation
  • Transform support: Works with schemas that use .transform() - uses input types for form fields

Using Without Default Values

The defaultValues parameter is optional. All form fields are automatically treated as optional during editing:

const schema = z.object({
  name: z.string().min(1),
  email: z.string().email(),
  age: z.number(),
});

// ✅ No defaultValues needed!
const form = useZodForm({ schema });

// Fields can be set individually as the user types
form.setValue("name", "John");
form.setValue("email", "[email protected]");
form.setValue("age", 25);

// Validation still enforces the schema on submit
const onSubmit = form.handleSubmit((data) => {
  // Type: { name: string; email: string; age: number }
  console.log(data);
});

This works because useZodForm uses the Simplify utility to ensure proper type inference, making all fields optional during editing while preserving exact types after validation.

Default Values Type Safety

The defaultValues parameter enforces shallow partial typing for type safety. This means:

  • Top-level fields can be omitted or set to null/undefined
  • Nested objects can be omitted entirely
  • Nested objects cannot be partially filled - they must be complete if provided
const schema = z.object({
  user: z.object({
    name: z.string(),
    email: z.string().email(),
    age: z.number(),
  }),
  settings: z.object({
    theme: z.enum(["light", "dark"]),
    notifications: z.boolean(),
  }),
});

// ✅ Correct: Omit nested objects
const form = useZodForm({
  schema,
  defaultValues: {
    // settings omitted - OK
  },
});

// ✅ Correct: Provide complete nested objects
const form = useZodForm({
  schema,
  defaultValues: {
    user: { name: "John", email: "[email protected]", age: 30 }, // Complete
    settings: { theme: "dark", notifications: true }, // Complete
  },
});

// ❌ TypeScript Error: Partial nested objects not allowed
const form = useZodForm({
  schema,
  defaultValues: {
    user: { name: "John" }, // ❌ Missing email and age
  },
});

Why this restriction? This prevents type errors where partial nested objects might be missing required properties. If you need to provide partial nested defaults, use getSchemaDefaults() which handles this correctly:

const form = useZodForm({
  schema,
  defaultValues: getSchemaDefaults(schema), // ✅ Type-safe partial defaults
});

Note: This restriction only applies to the defaultValues parameter. During form editing, all fields still accept null/undefined as expected:

// ✅ Works! Form inputs still accept null/undefined
form.setValue("user", null);
form.setValue("settings", null);
form.reset({ user: null, settings: undefined });

Custom Input Types

You can override the default input type transformation if needed:

import {
  useZodForm,
  PartialWithAllNullables,
} from "@zod-utils/react-hook-form";
import { z } from "zod";

const schema = z.object({
  username: z.string().min(3),
  email: z.string().email(),
  age: z.number(),
});

// Option 1: Use PartialWithAllNullables to make ALL fields accept null
const form = useZodForm<
  z.infer<typeof schema>,
  PartialWithAllNullables<z.infer<typeof schema>>
>({
  schema,
  defaultValues: { username: null, email: null, age: null },
});

// Option 2: Specify exact input types per field
const form2 = useZodForm<
  z.infer<typeof schema>,
  {
    username?: string | null; // Can be set to null
    email?: string; // Can only be undefined
    age?: number | null; // Can be set to null
  }
>({
  schema,
  defaultValues: { username: null, email: undefined, age: null },
});

Form Schema Context

The Form Schema Context system allows you to provide Zod schema context to deeply nested form components without prop drilling.

FormSchemaProvider

Provides schema context to all child components. Use this to wrap your form.

import { FormSchemaProvider } from "@zod-utils/react-hook-form";
import { z } from "zod";

const schema = z.object({
  username: z.string().min(3).max(20),
  email: z.string().email(),
});

function MyForm() {
  return (
    <FormSchemaProvider schema={schema}>
      <form>
        <UsernameField />
        <EmailField />
      </form>
    </FormSchemaProvider>
  );
}

With Discriminated Union

For discriminated unions, pass the discriminator to enable type-safe field access:

const schema = z.discriminatedUnion("mode", [
  z.object({ mode: z.literal("create"), name: z.string().min(1) }),
  z.object({ mode: z.literal("edit"), id: z.number() }),
]);

function CreateModeForm() {
  return (
    <FormSchemaProvider
      schema={schema}
      discriminator={{ key: "mode", value: "create" }}
    >
      <NameField /> {/* Only fields from 'create' variant are available */}
    </FormSchemaProvider>
  );
}

useFormSchema()

Access the schema context from child components:

import { useFormSchema } from "@zod-utils/react-hook-form";

function FieldComponent() {
  const context = useFormSchema();
  if (!context) return null;

  const { schema, discriminator } = context;
  // Use schema for field-level logic
}

useIsRequiredField({ schema, name, discriminator? })

Hook to check if a field requires valid input (shows validation errors on submit).

import { useIsRequiredField } from "@zod-utils/react-hook-form";

function FormLabel({ name, schema }: { name: string; schema: z.ZodType }) {
  const isRequired = useIsRequiredField({ schema, name });

  return (
    <label>
      {name}
      {isRequired && <span className="text-red-500">*</span>}
    </label>
  );
}

isRequiredField({ schema, name, discriminator? })

Standalone function to check if a field requires valid input:

import { isRequiredField } from "@zod-utils/react-hook-form";
import { z } from "zod";

const schema = z.object({
  username: z.string().min(1), // Required - min(1) rejects empty
  email: z.string(), // Not required - accepts empty string
  age: z.number(), // Required - numbers reject empty input
  bio: z.string().optional(), // Not required - optional
});

isRequiredField({ schema, name: "username" }); // true
isRequiredField({ schema, name: "email" }); // false
isRequiredField({ schema, name: "age" }); // true
isRequiredField({ schema, name: "bio" }); // false

useExtractFieldFromSchema({ schema, name, discriminator? })

Hook to extract a field's Zod schema from a parent schema. Memoized for performance.

import { useExtractFieldFromSchema } from "@zod-utils/react-hook-form";

function FieldInfo({ schema, name }: { schema: z.ZodType; name: string }) {
  const fieldSchema = useExtractFieldFromSchema({ schema, name });

  if (!fieldSchema) return null;

  // Use fieldSchema for custom validation or field info
  return <span>{fieldSchema._zod.def.typeName}</span>;
}

useFieldChecks({ schema, name, discriminator? })

Hook to get validation checks from a field's Zod schema. Useful for displaying validation hints like max length or min/max values.

import { useFieldChecks } from "@zod-utils/react-hook-form";

function FieldHint({ schema, name }: { schema: z.ZodType; name: string }) {
  const checks = useFieldChecks({ schema, name });

  const maxLength = checks.find((c) => c.check === "max_length");
  if (maxLength) {
    return <span>Max {maxLength.maximum} characters</span>;
  }
  return null;
}

Supported check types: min_length, max_length, greater_than, less_than, string_format, and more.


Core Utilities (Re-exported)

All utilities from @zod-utils/core are re-exported for convenience:

import {
  // Schema utilities (from @zod-utils/core)
  getSchemaDefaults,
  requiresValidInput,
  getPrimitiveType,
  removeDefault,
  extractDefaultValue,
  extendWithMeta,
  extractFieldFromSchema,
  getFieldChecks,
  type Simplify,
  type ZodUnionCheck,

  // Form schema context & hooks
  FormSchemaContext,
  FormSchemaProvider,
  useFormSchema,
  useIsRequiredField,
  isRequiredField,
  useExtractFieldFromSchema,
  useFieldChecks,

  // Form field utilities
  flattenFieldSelector,

  // Type utilities
  type PartialWithNullableObjects,
  type PartialWithAllNullables,
  type PartialFields,
  partialFields,
  type DiscriminatorProps,
  type DiscriminatorKey,
  type DiscriminatorValue,
  type SchemaProps,
  type SchemaAndDiscriminatorProps,
  type NameProps,
  type NameAndDiscriminatorProps,
  type FieldSelectorProps,
} from "@zod-utils/react-hook-form";

See @zod-utils/core documentation for details on schema utilities.

Type Utilities

PartialWithNullableObjects<T>

Transforms properties based on their type. By default, non-recursive - nested object fields stay strict.

Transformation rules:

  • Primitives (string, number, boolean): optional → type | undefined
  • Arrays: optional → type[] | undefined
  • Built-in objects (Date, RegExp, etc.): optional and nullable → type | null | undefined
  • Plain objects: optional and nullable, but nested fields stay strict{ strictField: type } | null | undefined
  • Objects marked with partialFields(): optional, nullable, and recursively transformed on direct fields
import type { PartialWithNullableObjects } from "@zod-utils/react-hook-form";

type User = {
  name: string;
  age: number;
  tags: string[];
  profile: { bio: string; settings: { theme: string } };
};

type FormInput = PartialWithNullableObjects<User>;
// {
//   name?: string;                        // Primitive: optional, not nullable
//   age?: number;                         // Primitive: optional, not nullable
//   tags?: string[];                      // Array: optional, not nullable
//   profile?: {                           // Object: optional, nullable
//     bio: string;                        // Nested field: STRICT (not optional)
//     settings: { theme: string };        // Nested object: STRICT
//   } | null;
// }

This is ideal for forms where nested objects come from selectors/dropdowns (should be complete when provided).

PartialWithAllNullables<T>

Makes all fields optional and nullable, but by default non-recursive - nested object fields stay strict.

Transformation rules:

  • Primitives: optional and nullable → type | null | undefined
  • Arrays: optional and nullable → type[] | null | undefined
  • Plain objects: optional and nullable, but nested fields stay strict
  • Objects marked with partialFields(): optional, nullable, and recursively transformed on direct fields
import type { PartialWithAllNullables } from "@zod-utils/react-hook-form";

type User = {
  name: string;
  age: number;
  profile: { bio: string };
};

type FormInput = PartialWithAllNullables<User>;
// {
//   name?: string | null;                 // Primitive: optional AND nullable
//   age?: number | null;                  // Primitive: optional AND nullable
//   profile?: { bio: string } | null;     // Object: nullable, but bio is STRICT
// }

partialFields(schema) - Opt-in Recursive Transformation

Use partialFields() to mark specific nested objects that should have their direct fields made partial. This is useful for objects where users fill in fields manually (vs. objects selected from dropdowns).

import { partialFields } from "@zod-utils/react-hook-form";
import { z } from "zod";

const schema = z.object({
  price: z.number(),
  // User fills in these fields - opt-in to partial
  detail: partialFields(
    z.object({
      hotel: z.string(),
      nights: z.number(),
    })
  ),
  // Selected from dropdown - stays strict
  agent: z.object({
    name: z.string(),
    fee: z.number(),
  }),
});

type FormInput = PartialWithNullableObjects<z.infer<typeof schema>>;
// {
//   price?: number;
//   detail?: {
//     hotel?: string;      // Partial - user input
//     nights?: number;     // Partial - user input
//   } | null;
//   agent?: {
//     name: string;        // STRICT - from selector
//     fee: number;         // STRICT - from selector
//   } | null;
// }

Note: partialFields() only affects the direct fields of the marked object. Nested objects within it will still stay strict unless they are also wrapped with partialFields().


flattenFieldSelector(params)

Flattens a FieldSelector into an array of primitive values for use in React dependency arrays.

import { flattenFieldSelector, extractFieldFromSchema } from "@zod-utils/react-hook-form";

function useFieldSchema(params) {
  return useMemo(() => {
    return extractFieldFromSchema(params);
  }, flattenFieldSelector(params));
}

Complete Example

import { useZodForm, getSchemaDefaults } from "@zod-utils/react-hook-form";
import { z } from "zod";

const userSchema = z.object({
  profile: z.object({
    firstName: z.string().min(1, "First name is required"),
    lastName: z.string().min(1, "Last name is required"),
    age: z.number().min(18).max(120),
  }),
  contact: z.object({
    email: z.string().email(),
    phone: z.string().optional(),
  }),
  preferences: z.object({
    theme: z.enum(["light", "dark"]).default("light"),
    notifications: z.boolean().default(true),
  }),
});

function UserForm() {
  const form = useZodForm({
    schema: userSchema,
    defaultValues: getSchemaDefaults(userSchema),
  });

  const onSubmit = form.handleSubmit((data) => {
    console.log("Valid data:", data);
  });

  return (
    <form onSubmit={onSubmit}>
      {/* Profile */}
      <input {...form.register("profile.firstName")} />
      {form.formState.errors.profile?.firstName && (
        <span>{form.formState.errors.profile.firstName.message}</span>
      )}

      <input {...form.register("profile.lastName")} />
      {form.formState.errors.profile?.lastName && (
        <span>{form.formState.errors.profile.lastName.message}</span>
      )}

      <input
        {...form.register("profile.age", { valueAsNumber: true })}
        type="number"
      />
      {form.formState.errors.profile?.age && (
        <span>{form.formState.errors.profile.age.message}</span>
      )}

      {/* Contact */}
      <input {...form.register("contact.email")} type="email" />
      {form.formState.errors.contact?.email && (
        <span>{form.formState.errors.contact.email.message}</span>
      )}

      <input {...form.register("contact.phone")} type="tel" />

      {/* Preferences - pre-filled with defaults */}
      <select {...form.register("preferences.theme")}>
        <option value="light">Light</option>
        <option value="dark">Dark</option>
      </select>

      <label>
        <input
          {...form.register("preferences.notifications")}
          type="checkbox"
        />
        Enable notifications
      </label>

      <button type="submit">Submit</button>
    </form>
  );
}

TypeScript Support

Fully typed with TypeScript for the best developer experience:

const form = useZodForm({
  schema: userSchema,
  defaultValues: getSchemaDefaults(userSchema),
});

// ✅ Fully typed
form.register("profile.firstName");

// ❌ TypeScript error
form.register("nonexistent.field");

License

MIT