---
title: Form Schema Validation with Zod and React Hook Form
description: We'll explore how to use Zod schema validation with React Hook Form in a TypeScript based React application.
slug: zod-typescript
authors: abdullah_numan
tags: [typescript]
image: https://refine.ams3.cdn.digitaloceanspaces.com/blog/2024-05-21-zod/social.png
hide_table_of_contents: false
---

## Introduction

Form field validation with schema is an important aspect of implementing advanced forms in a frontend application. A form schema helps define concrete shape of the data handled by a form.

In a TypeScript based React form, schema validation involves proper type declaration and annotation of the database entity handled. As data entities or resources in an application grows, type overlap, mapping, interconversion, derivation and other manipulation becomes inevitable. This makes static typing of form schema from scratch cumbersome in a growing codebase.

[Zod](https://zod.dev/) is a TypeScript-first library that addresses these issues. It provides a comprehensive list of battle tested APIs for declaring and applying well typed form schema validations in a React based form. With its validator methods for declaring primitives, objects, and schema derivation APIs that mirror those in TypeScript, Zod makes static typing of form schema extremely versatile. Zod comes with handful of extra features for implementing highly specific validation rules with its validator precision, refinement and transformation methods. It is a dependency-free library that complements well with leading React based form solutions such as [Formik](https://formik.org/) and [React Hook Form](https://react-hook-form.com/).

In this post, we demonstrate how to use some of the major Zod APIs for implementing properly typed form schema validations with Zod and React Hook Form, in a TypeScript based React application.
This post is about how to implement Zod schema validation with React Hook Form. We cover the basics with an example that adopts Zod on top of plain React Hook Form, as well as some important Zod APIs that allow more refined schema validations.

## Overview

We first make sense of what a form schema is, its importance in large code bases and why we need proper static typing of form schema validations in a TypeScript based React application. We relate how Zod helps implement well typed form schema in a React application and discuss the role of the Zod resolver for React Hook Form in integrating Zod schemas into React Hook Form validations.

We cover Zod basics while migrating a form in an existing plain React Hook Form based app to Zod. We elaborate, with examples from a `Create Post` form, what a `zod` schema and validators are, and how to use Zod primitives (such as `string`) to declare validators. We learn how they help compose complex data structures with the `object()` method, and demonstrate examples of Zod's validator precision APIs such as `min()`, `max()`, `email()`, etc. We explore how the `parse()` method plays a central role in deciding the runtime schema used for validations. We also examine how Zod types are generated from a schema with the `infer()` API.

In the later half of the post, we consider an `<EditProfile />` form component to implement Zod default values with the `default()` method. We cover how Zod allows intuitive derivation of schemas via TypeScript-like utilities such as `partial()`, `pick()` and `omit()`. We use them to help easily implement complex form field requirements that are normally time consuming otherwise.

Towards the end, we discuss Zod refinements and transformations. We examine examples of composing custom accurate rules with the `refine()` method and transforming field values with `transform()`.

## Prerequisites

In order to properly follow this post and test out the examples, we expect that you come familiar with Formik or React Hook Form based React applications. If you have not worked with any of these yet, it would be useful to code along with the React Hook Form application in this Refinedev Blog post on [Essentials of Managing Form State with React Hook Form](https://refine.dev/blog/react-hook-form/).

It is also expected that you are familiar with basic type declaration, annotation in TypeScript as well as some type transformation utilities [here](https://www.typescriptlang.org/docs/handbook/utility-types.html#partialtype).

For this post, we start off with the code in [this repository](https://github.com/anewman15/rhf-zod). We make necessary changes during the adoption of Zod, and present relevant snippets while explaining underlying concepts.

## What is Zod ?

[Zod](https://zod.dev/) is a Typescript-first form schema declaration and validation library. Zod can be used in any React application. It's API is dedicated to declaring and deriving type safe schemas for database entities that are commonly handled in typical resource based form applications.

### Why We Should Use Form Schemas

Form schemas in general are beneficial as they are focused on a data entity handled in a form. Schemas make form field declaration and validations easy to implement. Schemas are also useful for writing DRY (Don't Repeat Yourself). They contribute to code stability, maintainability and scalability by keeping declarations consistent as application entities increase.

### Why Zod is Special ?

Zod, as a well tested schema validation library, is useful in implementing feature rich form experiences with versatile solutions like Formik and React Hook Form.

Other schema validation libraries like [Yup](https://github.com/jquense/yup) and [Joi](https://joi.dev/) exist. However, what sets Zod apart is its extensive static typing API surface that mirrors TypeScript's APIs for type declaration, derivation, inference and other sorts of manipulation that works in combination with the data itself. This makes Zod extremely friendly in growing TypeScript code-planets, where type mapping, derivation and data manipulation become unavoidable as application entities increase.

### How Zod Works

Here's how Zod works:

- Zod gives a Zod **_instance_** with the `zod` object (or any other identifier) which exposes APIs for declaring validators.
- **Zod validators** are individual validation declarations. The simplest of validators would be a primitive type and have relevant error messages. A validator in Zod typically represents a form field or property of a database entity. Zod's validators can add necessary precision rules, rule refinements and / or transformations.
- Zod **_primitives_** are validator declaration methods that represent typical JS / TS primitives such as `string`s, `number`s, `boolean`s, etc. A `string` validator is declared with the `string()` method, a `number` with the `number()` method on the Zod instance, and so on.
- Zod **`object` schemas** represent a database entity with all its individual attributes. Zod object schemas are initialized with the `object()` method and composed from primitives.
- Data precision in Zod can be implemented with **precision validator** APIs. For example, we can implement a `string`'s precision with `min()`, `max()`, `email()` and others.
- Zod **schema derivation / manipulation** can be done according to need with full TypeScript support. For example, derivation with `partial()`, `pick()` and `omit()` are common.
- **Validator refinements** with custom requirements can be implemented to run nuanced form validations. Zod offers refinements with `refine()` and `superRefine()`.
- Form field data can be transformed with **Zod transformations**. The `transform()` method is used for this.
- **Type generation** from a Zod schema are done with the `infer()` method on the schema.
- Zod **validation runs** are performed with the `parse()` method. Running the validations checks for accuracy of the form field data according to declared validators. Any error is returned to Zod schema / form instance.

### Zod Resolver for React Hook Form

When using Zod with React Hook Form, we need to use the Zod resolver for React Hook Form. React Hook Form supports Zod via its [Zod Resolver](https://github.com/react-hook-form/resolvers?tab=readme-ov-file#zod) package. The job of the Zod resolver is to trigger validations in accordance with events taking place in a React Hook Form, translate executed validations and return the result (success or failures) to React Hook Form's instance.

## Zod Schema Validation with TypeScript: How to Migrate from Plain React Hook Form

In this section, we adopt Zod schema validations in an existing plain React Hook Form based form. The code for the application is available in [this repository](https://github.com/anewman15/rhf-zod). We suggest you make a local clone and then code along from there.

### Starter Files

The form we are going to apply Zod on is inside the `App.tsx` file. It contains a fully functioning RHF based `Create Post` form. We expect you are already familiar with the code here.

If not, please follow this Refinedev blog post [here](https://refine.dev/blog/react-hook-form/).

#### The `App.tsx` Component

The code for the `App.tsx` file is given below:

<details>

<summary>Show App.tsx code</summary>

```tsx title="./src/App.tsx"
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import "./App.css";

function App() {
  const formInstance = useForm({
    mode: "onChange",
    defaultValues: {
      title: "",
      subtitle: "",
      content: "",
    },
    criteriaMode: "all",
    shouldFocusError: true,
  });

  useEffect(() => {
    formInstance?.reset();
  }, []);

  return (
    <div className="flex min-h-screen w-full items-center justify-center dark:bg-gray-950">
      <div className="max-w-md rounded-lg bg-white px-8 py-6 shadow-md dark:bg-gray-900">
        <h1 className="mb-4 text-center text-2xl font-bold dark:text-gray-200">
          Create Post
        </h1>
        <form
          onSubmit={formInstance?.handleSubmit((data) => {
            setTimeout(() => {
              console.log("data", data);
              formInstance?.setError("subtitle", {
                message: new Error("Server Error: Subtitle field is protected")
                  .message,
              });
            }, 2000);
          })}
        >
          <div className="mb-4">
            <label className="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
              Title
            </label>
            <input
              {...formInstance?.register("title", {
                required: "Post title cannot be empty",
              })}
              type="text"
              className="w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-indigo-500"
              placeholder="Add post title"
            />
            {formInstance?.formState.errors?.title && (
              <span className="text-xs text-red-500">
                {formInstance?.formState.errors?.title?.message}
              </span>
            )}
          </div>
          <div className="mb-4">
            <label className="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
              Subtitle
            </label>
            <input
              type="text"
              className="w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-indigo-500"
              placeholder="Add a subtitle"
              {...formInstance?.register("subtitle", {
                maxLength: {
                  value: 65,
                  message: "Keep subtitle shorter",
                },
              })}
            />
            {formInstance?.formState.errors?.subtitle && (
              <span className="text-xs text-red-500">
                {formInstance?.formState.errors?.subtitle?.message}
              </span>
            )}
          </div>
          <div className="mb-4">
            <label className="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
              Content
            </label>
            <textarea
              cols={40}
              rows={5}
              className="w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-indigo-500"
              placeholder="Add content here"
              {...formInstance?.register("content", {
                required: "Content cannot be empty",
                minLength: {
                  value: 20,
                  message: "Content should have enough information",
                },
                maxLength: {
                  value: 1000,
                  message:
                    "Content has reached maximum limit of 1000 characters",
                },
              })}
            ></textarea>
            {formInstance?.formState.errors?.content && (
              <span className="text-xs text-red-500">
                {formInstance?.formState.errors?.content?.message}
              </span>
            )}
          </div>
          <div className="flex justify-between">
            <button
              disabled={!formInstance?.formState?.isValid}
              type="submit"
              className="flex w-40 justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 disabled:bg-gray-300"
            >
              Create Post
            </button>
          </div>
        </form>
      </div>
    </div>
  );
}

export default App;
```

</details>

This form uses a React Hook Form instance with the following configurations:

```ts
const formInstance = useForm({
  mode: "onChange",
  // highlight-start
  defaultValues: {
    title: "",
    subtitle: "",
    content: "",
  },
  // highlight-end
  criteriaMode: "all",
  shouldFocusError: true,
});
```

Notice, the form is in `onChange` mode, which runs validations on each value change. More importantly, we have `defaultValues` set, which lets TypeScript infer the shape of the entire form's data -- in effect, the schema.

The form fields use React Hook Form's native validation rules set on each form field with the `register()` API.

For example:

```tsx
<input
  // highlight-start
  {...formInstance?.register("title", {
    required: "Post title cannot be empty",
  })}
  // highlight-end
  type="text"
  className="w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-indigo-500"
  placeholder="Add post title"
/>
```

Notice also, that the React Hook Form `formInstance` `handleSubmit()` handler on the `onSubmit` event in `<form>` element:

```ts
<form
  onSubmit={formInstance?.handleSubmit((data) => {
    setTimeout(() => {
      console.log("data", data);
      formInstance?.setError("subtitle", {
        message: new Error("Server Error: Subtitle field is protected").message,
      });
    }, 2000);
  })}
></form>
```

It would typically be some data fetching function that uses `fetch()` API, React Query mutation, or Axios that performs a `POST` or `PUT/PATCH` method. For the purpose of this demonstration, we emulate a dummy server side error integration on `subtitle` field.

Please feel free to play around with the features and test out the validations. We'll compare them while replacing the rules with Zod schema validators:

 <div className="centered-image">
<img src="https://refine.ams3.cdn.digitaloceanspaces.com/blog/2024-05-21-zod/zod-1.gif" alt="typescript zod" />
</div>

In the following sections, we migrate the above React Hook Form based form to Zod. Before we make the necessary changes, we need to install the packages.

### Zod with React Hook Form: Installing Packages

We need to install the `npm` package for Zod and its dependencies. Run the following command:

```bash
npm install zod @hookform/resolvers
```

This should place both packages inside `package.json`. Take special note of `@hookform-resolvers` package. This is an integration package for using schema validation libraries with React Hook Form. We need to use the Zod resolver from this package. Otherwise, Zod alone won't work with React Hook Form.

### How to Implement Zod Schema Validation with React Hook Form

Now, let's make changes to the existing form. In the below adoption, we instantiate a Zod `object` schema with individual validators declared for the form fields:

<details>

<summary>Show updated `App.tsx` with Zod schema</summary>

```tsx title="./src/App.tsx"
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import * as zod from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import "./App.css";

function App() {
  const subtitle = zod.string().max(65, { message: "Keep subtitle shorter" });
  const content = zod
    .string()
    .min(20, { message: "Content should have enough information" })
    .max(1000, {
      message: "Content has reached maximum limit of 1000 characters",
    });

  const PostSchema = zod.object({
    title: zod.string().min(1, { message: "Title cannot be empty" }),
    subtitle,
    content,
  });

  type TPost = zod.infer<typeof PostSchema>;

  const formInstance = useForm({
    resolver: zodResolver(PostSchema),
    mode: "onChange",
    defaultValues: {
      title: "",
      subtitle: "",
      content: "",
    },
    criteriaMode: "all",
    shouldFocusError: true,
  });

  useEffect(() => {
    formInstance?.reset();
  }, []);

  return (
    <div className="flex min-h-screen w-full items-center justify-center dark:bg-gray-950">
      <div className="max-w-md rounded-lg bg-white px-8 py-6 shadow-md dark:bg-gray-900">
        <h1 className="mb-4 text-center text-2xl font-bold dark:text-gray-200">
          Create Post
        </h1>
        <form
          onSubmit={formInstance?.handleSubmit((data) => {
            setTimeout(() => {
              console.log("data", data);
              formInstance?.setError("subtitle", {
                message: new Error("Server Error: Subtitle field is protected")
                  .message,
              });
            }, 2000);
          })}
        >
          <div className="mb-4">
            <label className="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
              Title
            </label>
            <input
              {...formInstance?.register("title")}
              type="text"
              className="w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-indigo-500"
              placeholder="Add post title"
            />
            {formInstance?.formState.errors?.title && (
              <span className="text-xs text-red-500">
                {formInstance?.formState.errors?.title?.message}
              </span>
            )}
          </div>
          <div className="mb-4">
            <label className="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
              Subtitle
            </label>
            <input
              type="text"
              className="w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-indigo-500"
              placeholder="Add a subtitle"
              {...formInstance?.register("subtitle")}
            />
            {formInstance?.formState.errors?.subtitle && (
              <span className="text-xs text-red-500">
                {formInstance?.formState.errors?.subtitle?.message}
              </span>
            )}
          </div>
          <div className="mb-4">
            <label className="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
              Content
            </label>
            <textarea
              cols={40}
              rows={5}
              className="w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-indigo-500"
              placeholder="Add content here"
              {...formInstance?.register("content")}
            ></textarea>
            {formInstance?.formState.errors?.content && (
              <span className="text-xs text-red-500">
                {formInstance?.formState.errors?.content?.message}
              </span>
            )}
          </div>
          <div className="flex justify-between">
            <button
              disabled={!formInstance?.formState?.isValid}
              type="submit"
              className="flex w-40 justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 disabled:bg-gray-300"
            >
              Create Post
            </button>
          </div>
        </form>
      </div>
    </div>
  );
}

export default App;
```

</details>

Notice, we have applied Zod validation rules in a `PostSchema` object and we have passed it to `useForm()`'s `resolver` configuration property.

Later inside the JSX, we have removed React Hook Form native validation rules from each form field:

```ts
<input
  // highlight-next-line
  {...formInstance?.register("title")} // Field needs to be only registered, with no rules passed
  type="text"
  placeholder="Add post title"
/>
```

This is because, React Hook Form now relies on the Zod Resolver to handle validations.

In the sections below, we examine the Zod related concepts and explain them in snippets.

#### Zod with React Hook Form: How to Use an Object Schema

In the above changes, we have instantiated a `PostSchema` object with `zod.object()` method:

```ts
const PostSchema = zod.object({
  title: zod.string().min(1, { message: "Title cannot be empty" }),
  subtitle,
  content,
});
```

An object schema typically represents a database entity with its properties. So, the fields here stand for the properties of the data entity.

We have to then pass this Zod schema to `useForm()` React Hook Form hook, to configure the value of the `resolver` option with the `zodResolver()` function:

```ts
const formInstance = useForm({
  resolver: zodResolver(PostSchema),
  // other config options
});
```

Adding the schema declaration object to `useForm()` with `zodResolver()` integrates it to React Hook Form's form instance. All Zod validations are run according to React Hook Form configurations, and errors are returned to the `formInstance`.

#### How to Declare Zod Validators

Notice, we have used a validator inside the `PostSchema` object.

```ts
title: zod.string().min(1, { message: "Title cannot be empty" }),
```

Zod validators help declare individual validation rules for form fields. They represent a single property in the database entity.

As you can see in the above `title` attribute, we can declare a validator inside an `object` schema.

We can declare field level schemas seprarately as well. As with `subtitle` and `content`:

```ts
const subtitle = zod.string().max(65, { message: "Keep subtitle shorter" });
const content = zod
  .string()
  .min(20, { message: "Content should have enough information" })
  .max(1000, {
    message: "Content has reached maximum limit of 1000 characters",
  });
```

Notice, we can chain validators with their respectie methods. In `subtitle` and `content`, we have chained the `min()` and `max()` validators to `string()`.

#### Zod Validator Syntax: Zod Primitives and Field Precision Validators

In the validator declarations above, we have used a `string()` primitive method that represents the TypeScript `string` primitive type. Primitives decide the TypeScript type of the form field entry. Zod has support for all TypeScript primitive types. A full list can be found [here](https://zod.dev/?id=primitives).

On top of primitives, we can impose field precision rules with specifiers such as `min()` and `max()`. The syntax for each validator starts with the primitive, followed by chained precision validators.

Notice that a precision validator follows an intuitive syntax. It takes the `message` in an object, after the specifier value:

```ts
.min(20, { message: "Content should have enough information" })
.max(1000, { message: "Content has reached maximum limit of 1000 characters" });
```

With the changes above, we get the same validations we implemented in the original code. We have effectively replaced Reach Hook Form validation rules with Zod schemas.

Notice that, other features of React Hook Form, such as server error integration in the `handleSubmit` callback we used on `onSubmit` event, remain unaffected:

 <div className="centered-image">
<img src="https://refine.ams3.cdn.digitaloceanspaces.com/blog/2024-05-21-zod/zod-2.png" alt="typescript zod validator syntax" />
</div>

#### Zod Validation Rules Parsing

Zod parses validation rules according to configurations set in React Hook Form strategy and revalidation. By default, it invokes the `parse()` method on configured React Hook Form events. Parsing takes place thanks to the `zodResolver` which acts as middleman between Zod and hook.

Apart from that, validation runs can be triggered manually by calling `safeParse()` . The `safeParse()` method is called on the schema. It accepts the schema or form field data. If the data passes all validations, it returns an object with `success: true` and associated data. Otherwise, it returns `success: false` and the stack information without breaking the app:

```ts
PostSchema.safeParse({
  title: "General Zod of Candor",
  subtitle: "Executing...",
  content: "Kneel before Kal El.",
}); // Returns { success: true, data: {...} }

PostSchema.safeParse({
  title: "General Zod of Candor",
  subtitle: "Executing... Running now and forever",
  content: "Kneel",
}); // Returns { success: false, error: [...] }
```

:::warning

We can use the Zod `parse()` method to trigger validations. However, `parse()` is not safe as it throws a ZodError that breaks the application. In such a case, we have to handle errors gracefully inside a `try...catch` block. You can find more information about `parse()` [here](https://zod.dev/?id=parse).

:::

#### Zod `infer()`: How to Infer Schema Types

Zod sets static types for all schema declared on the `zod` instance. The type for a schema can be produced with the `infer<>` method.

For example, we can store the static type for `PostSchema` like this:

```ts
type TPost = zod.infer<typeof PostSchema>;
/*
  {
    title: string;
    subtitle: string;
    content: string;
  }
*/
```

`TPost` can then be used for annotating the `post` resource elsewhere.

### Elaborate Zod Stuff with React Hook Form: An Edit Profile Example

In this section, we work with a form for `<EditProfile />` component. While doing so, we explore some nuanced Zod schema features.

The code for the `<EditProfile />` component looks like this:

<details>

<summary>Show `<EditProfile />` component code</summary>

```tsx title="./src/components/edit-profile.tsx"
import { ReactNode, useEffect } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import * as zod from "zod";

export function EditProfile() {
  const first_name = zod
    .string()
    .min(1, { message: "First name cannot be empty" })
    .max(50, { message: "Really? This long ?" })
    .default("Dru");

  const last_name = zod
    .string()
    .min(1, { message: "Last name cannot be empty" })
    .max(50, { message: "Really? This long ?" })
    .default("Zod");

  const email = zod
    .string()
    .email()
    .refine((e) => e.slice(e.length - 3).includes(".kr"), {
      message: "This should be a Kryptonian email",
    })
    .default("general.zod@candor.mil.kr");

  const website = email.transform((e) => `https://${e.split("@")?.[1]}`);
  const ProfileSchema = zod.object({
    username: zod
      .string()
      .transform((u) => u.split(" ").join("_"))
      .default("general_zod"),
    first_name,
    last_name,
    email,
  });

  const formInstance = useForm({
    resolver: zodResolver(ProfileSchema),
    mode: "onChange",
    defaultValues: ProfileSchema.parse({}),
    criteriaMode: "all",
    shouldFocusError: true,
    reValidateMode: "onSubmit",
  });

  useEffect(() => {
    formInstance?.reset();
  }, []);

  return (
    <div className="flex min-h-screen w-full items-center justify-center dark:bg-gray-950">
      <div className="max-w-md rounded-lg bg-white px-8 py-6 shadow-md dark:bg-gray-900">
        <h1 className="mb-4 text-center text-2xl font-bold dark:text-gray-200">
          Edit Profile
        </h1>
        <form
          onSubmit={formInstance?.handleSubmit((data) => {
            setTimeout(() => {
              console.log("data", data);
            }, 2000);
          })}
        >
          <div className="mb-4">
            <label className="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
              Username
            </label>
            <input
              {...formInstance?.register("username")}
              type="text"
              className="w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-indigo-500"
              placeholder="Choose a username"
            />
            {formInstance?.formState.errors?.username && (
              <span className="text-xs text-red-500">
                {formInstance?.formState.errors?.username?.message as ReactNode}
              </span>
            )}
          </div>
          <div className="mb-4">
            <label className="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
              First Name
            </label>
            <input
              type="text"
              className="w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-indigo-500"
              placeholder="First name"
              {...formInstance?.register("first_name")}
            />
            {formInstance?.formState.errors?.first_name && (
              <span className="text-xs text-red-500">
                {
                  formInstance?.formState.errors?.first_name
                    ?.message as ReactNode
                }
              </span>
            )}
          </div>
          <div className="mb-4">
            <label className="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
              Last Name
            </label>
            <input
              type="text"
              className="w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-indigo-500"
              placeholder="Last name"
              {...formInstance?.register("last_name")}
            />
            {formInstance?.formState.errors?.last_name && (
              <span className="text-xs text-red-500">
                {
                  formInstance?.formState.errors?.last_name
                    ?.message as ReactNode
                }
              </span>
            )}
          </div>
          <div className="mb-4">
            <label className="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
              Email
            </label>
            <input
              type="text"
              className="w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-indigo-500"
              placeholder="email@exmaple.kr"
              {...formInstance?.register("email")}
            ></input>
            {formInstance?.formState.errors?.email && (
              <span className="text-xs text-red-500">
                {formInstance?.formState.errors?.email?.message as ReactNode}
              </span>
            )}
          </div>
          <div className="flex justify-between">
            <button
              type="submit"
              className="flex w-40 justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 disabled:bg-gray-300"
            >
              Save Changes
            </button>
          </div>
        </form>
      </div>
    </div>
  );
}
```

</details>

You can import this component into `App.tsx` and then place it inside the JSX.

Let's now examine and discuss the Zod concepts implemented in this form.

#### Zod Default Values

In Zod, we have to specify default values on the validator itself, with the `default()` method. Like this:

```ts
const first_name = zod
  .string()
  .min(1, { message: "First name cannot be empty" })
  .max(50, { message: "Really? This long ?" })
  // highlight-next-line
  .default("Dru");

const last_name = zod
  .string()
  .min(1, { message: "Last name cannot be empty" })
  .max(50, { message: "Really? This long ?" })
  // highlight-next-line
  .default("Zod");
```

These, however, do not get relayed to React Hook Form's `defaultValues` attribute, so they do not get displayed on form fields.

For them to show up in the form fields, we have to parse the schema and then pass the output to `useForm()`'s `defaultValues` configuration:

```ts
const formInstance = useForm({
  resolver: zodResolver(ProfileSchema),
  mode: "onChange",
  // highlight-next-line
  defaultValues: ProfileSchema.parse({}),
  criteriaMode: "all",
  shouldFocusError: true,
  reValidateMode: "onSubmit",
});
```

With the empty object passed to `ProfileSchema.parse({})`, the default values specified with Zod `defaultValue()` before are set to their fields.

#### Zod Schema Derivation

Zod supports schema derivation and related type manipulations. For example, in cases where we need to set some properties to `optional`, we can apply the `partial()` method to derive a new type:

```ts
const ProfileOptional = ProfileSchema.partial();
const ProfileOptionalLastName = ProfileSchema.partial({
  last_name: true,
});

type TProfileOptional = zod.infer<typeof ProfileOptional>;
/*
  type TProfileOptional = {
    username?: string | undefined;
    first_name?: string | undefined;
    last_name?: string | undefined;
    email?: string | undefined;
  };
*/

type TProfileOptionalLastName = zod.infer<typeof ProfileOptionalLastName>;
/*
  type TProfileOptionalLastName = {
    username: string;
    first_name: string;
    email: string;
    last_name?: string | undefined;
  };
*/
```

Notice, with no arguments passed, we applied partial flag to all items. We can make an individual field optional by passing setting it to `true`.

:::tip

Zod schema derivation utilities are used to manipulate form schema. They mirror TypeScript utilities with similar names and produce static types that they represent in TypeScript.

For example, `pick()` produces a type with the same type transformation impact as TypeScript `Pick<>`. `omit()` brings the same type derivations as TypeScript `Omit<>`.

:::

#### Zod Refinements

Zod refinements allow us to attain fine grained specificity in validation rules, which is not normally possible with primitives and field precision methods.

For example, we imposed our `email` field to be an `email()`. And we want it to be only a Kryptonian email, with last three characters being `.kr`:

```ts
const email = zod
  .string()
  .email()
  // highlight-next-line
  .refine((e) => e.slice(e.length - 3).includes(".kr"), {
    message: "This should be a Kryptonian email",
  })
  .default("general.zod@candor.mil.kr");
```

 <div className="centered-image">
<img src="https://refine.ams3.cdn.digitaloceanspaces.com/blog/2024-05-21-zod/zod-3.png" alt="typescript zod refinements" />
</div>

:::tip

The `refine()` method is a convenient method for implementing very specific validation rules. More verbose validators can be defined with the `superRefine()` method.

For details, see the docs [here](https://zod.dev/?id=superrefine).

:::

#### Zod Field Value Transformations

Zod transformations allow us to transform the value of a form field.

For example, in the `username` field, we want to convert `&nbsp;` (space) into an `&lowbar;` (underscore):

```ts
const ProfileSchema = zod.object({
  // highlight-next-line
  username: zod
    .string()
    .transform((u) => u.split(" ").join("_"))
    .default("general_zod"),
  first_name,
  last_name,
  email,
});
```

In this snippet, we are transforming the input so that all spaces are replaced by an underscore. The transformed data is stored inside the `formInstance`. We have logged the form data to console inside the `handleSubmit` callback. You can verify this when you look at the console after submitting the form. The `username` field does not contain any space -- as all of them are turned into a lowbar:

```ts
{
  // highlight-next-line
  "username": "general_dru_zod",
  "first_name": "Dru",
  "last_name": "Zod",
  "email": "general.zod@candor.mil.kr"
}
```

As you can see, Zod refinements and transformations are useful for easily implementing subtle form requirements that are difficult to implement from scratch.

## Summary

In this post, we learned about how to implement Zod schema validations in a React Hook Form based application.

We first explored what schema validations are, why we need them and how Zod provides battle tested solutions for type safe schemas in a React Hook application with Zod Resolver.

With an example of migrating existing `Create Post` React Hook Form based validations, we learned about how to declare Zod validators with `string` primitives and compose object schemas from them. We saw examples of using `string` related field precision validators such as `min()`, `max()` and `email()`. We also made sense of how Zod produces TypeScript types from a schema with `infer()` and how custom parsing is executed with the `parse()` and `safeParse()` methods.

Later on, we implemented subtle form features that are easily made possibly by Zod. We added default values the Zod way with the `default()` method. With `refine()` API, we implemented a custom validator that imposes an `email()` field to belong to `.kr`. Finally, we learned how the `transform()` method helps us convert a field data to something of our liking to be included in the form data set.
