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 🙏

© 2025 – Pkg Stats / Ryan Hefner

@zod-utils/react-hook-form

v2.0.2

Published

React Hook Form integration and utilities for Zod schemas

Downloads

1,262

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). Returns false if schema or name is not provided, making it safe to use in conditional contexts.

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,

  // Type utilities
  type PartialWithNullableObjects,
  type PartialWithAllNullables,
  type Discriminator,
  type DiscriminatorKey,
  type DiscriminatorValue,
  type InferredFieldValues,
  type ValidFieldPaths,
  type ValidFieldPathsOfType,
} 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. Primitive and array fields become optional-only (not nullable), while object fields become optional and nullable.

Transformation rules:

  • Primitives (string, number, boolean): optional → type | undefined
  • Arrays: optional → type[] | undefined
  • Objects: optional and nullable → type | null | undefined
import type { PartialWithNullableObjects } from "@zod-utils/react-hook-form";

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

type FormInput = PartialWithNullableObjects<User>;
// {
//   name?: string;                  // Primitive: optional, not nullable
//   age?: number;                   // Primitive: optional, not nullable
//   tags?: string[];                // Array: optional, not nullable
//   profile?: { bio: string } | null; // Object: optional AND nullable
// }

This type is used internally by useZodForm to allow form fields to accept undefined (and null for objects only) during editing while maintaining proper validation types.

PartialWithAllNullables<T>

Makes all fields optional and nullable, regardless of type.

Transformation rules:

  • All fields: optional and nullable → type | null | undefined
import type { PartialWithAllNullables } from "@zod-utils/react-hook-form";

type User = {
  name: string;
  age: number;
  tags: string[];
};

type FormInput = PartialWithAllNullables<User>;
// {
//   name?: string | null;       // All fields: optional AND nullable
//   age?: number | null;        // All fields: optional AND nullable
//   tags?: string[] | null;     // All fields: optional AND nullable
// }

Use this when all fields need to accept null, not just objects/arrays.


ValidFieldPathsOfType<TSchema, TValueConstraint, TDiscriminatorKey?, TDiscriminatorValue?, TFieldValues?>

Extracts field paths where the value type matches a constraint. Useful for building type-safe form components that only accept paths of specific types.

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

const schema = z.object({
  name: z.string(),
  age: z.number(),
  score: z.number().optional(),
  active: z.boolean(),
});

// Only accept number field paths
type NumberPaths = ValidFieldPathsOfType<typeof schema, number>;
// "age" | "score"

// Only accept boolean field paths
type BooleanPaths = ValidFieldPathsOfType<typeof schema, boolean>;
// "active"

Use case - Type-safe form field components:

// NumberFormField only accepts paths to number fields
function NumberFormField<
  TSchema extends z.ZodType,
  TPath extends ValidFieldPathsOfType<TSchema, number>
>({ schema, name }: { schema: TSchema; name: TPath }) {
  // name is guaranteed to be a path to a number field
  return <input type="number" name={name} />;
}

// ✅ Works - 'age' is a number field
<NumberFormField schema={schema} name="age" />

// ❌ TypeScript error - 'name' is a string field, not number
<NumberFormField schema={schema} name="name" />

With discriminated unions:

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

// Number paths for 'edit' variant
type EditNumberPaths = ValidFieldPathsOfType<
  typeof formSchema,
  number,
  "mode",
  "edit"
>;
// "id" | "rating"

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