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 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 and React Hook Form.
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.
It is also expected that you are familiar with basic type declaration, annotation in TypeScript as well as some type transformation utilities here.
For this post, we start off with the code in this repository. We make necessary changes during the adoption of Zod, and present relevant snippets while explaining underlying concepts.
What is Zod ?
Zod 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 and Joi 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. Astring
validator is declared with thestring()
method, anumber
with thenumber()
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 theobject()
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 withmin()
,max()
,email()
and others. - Zod schema derivation / manipulation can be done according to need with full TypeScript support. For example, derivation with
partial()
,pick()
andomit()
are common. - Validator refinements with custom requirements can be implemented to run nuanced form validations. Zod offers refinements with
refine()
andsuperRefine()
. - 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 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. 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.
The App.tsx
Component
The code for the App.tsx
file is given below:
Show App.tsx code
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="min-h-screen flex items-center justify-center w-full dark:bg-gray-950">
<div className="bg-white dark:bg-gray-900 shadow-md rounded-lg px-8 py-6 max-w-md">
<h1 className="text-2xl font-bold text-center mb-4 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="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Title
</label>
<input
{...formInstance?.register("title", {
required: "Post title cannot be empty",
})}
type="text"
className="shadow-sm rounded-md w-full px-3 py-2 border border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
placeholder="Add post title"
/>
{formInstance?.formState.errors?.title && (
<span className="text-red-500 text-xs">
{formInstance?.formState.errors?.title?.message}
</span>
)}
</div>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Subtitle
</label>
<input
type="text"
className="shadow-sm rounded-md w-full px-3 py-2 border border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
placeholder="Add a subtitle"
{...formInstance?.register("subtitle", {
maxLength: {
value: 65,
message: "Keep subtitle shorter",
},
})}
/>
{formInstance?.formState.errors?.subtitle && (
<span className="text-red-500 text-xs">
{formInstance?.formState.errors?.subtitle?.message}
</span>
)}
</div>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Content
</label>
<textarea
cols={40}
rows={5}
className="shadow-sm rounded-md w-full px-3 py-2 border border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-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-red-500 text-xs">
{formInstance?.formState.errors?.content?.message}
</span>
)}
</div>
<div className="flex justify-between">
<button
disabled={!formInstance?.formState?.isValid}
type="submit"
className="w-40 disabled:bg-gray-300 flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
Create Post
</button>
</div>
</form>
</div>
</div>
);
}
export default App;
This form uses a React Hook Form instance with the following configurations:
const formInstance = useForm({
mode: "onChange",
defaultValues: {
title: "",
subtitle: "",
content: "",
},
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:
<input
{...formInstance?.register("title", {
required: "Post title cannot be empty",
})}
type="text"
className="shadow-sm rounded-md w-full px-3 py-2 border border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
placeholder="Add post title"
/>
Notice also, that the React Hook Form formInstance
handleSubmit()
handler on the onSubmit
event in <form>
element:
<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:
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:
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:
Show updated `App.tsx` with Zod schema
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="min-h-screen flex items-center justify-center w-full dark:bg-gray-950">
<div className="bg-white dark:bg-gray-900 shadow-md rounded-lg px-8 py-6 max-w-md">
<h1 className="text-2xl font-bold text-center mb-4 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="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Title
</label>
<input
{...formInstance?.register("title")}
type="text"
className="shadow-sm rounded-md w-full px-3 py-2 border border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
placeholder="Add post title"
/>
{formInstance?.formState.errors?.title && (
<span className="text-red-500 text-xs">
{formInstance?.formState.errors?.title?.message}
</span>
)}
</div>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Subtitle
</label>
<input
type="text"
className="shadow-sm rounded-md w-full px-3 py-2 border border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
placeholder="Add a subtitle"
{...formInstance?.register("subtitle")}
/>
{formInstance?.formState.errors?.subtitle && (
<span className="text-red-500 text-xs">
{formInstance?.formState.errors?.subtitle?.message}
</span>
)}
</div>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Content
</label>
<textarea
cols={40}
rows={5}
className="shadow-sm rounded-md w-full px-3 py-2 border border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
placeholder="Add content here"
{...formInstance?.register("content")}
></textarea>
{formInstance?.formState.errors?.content && (
<span className="text-red-500 text-xs">
{formInstance?.formState.errors?.content?.message}
</span>
)}
</div>
<div className="flex justify-between">
<button
disabled={!formInstance?.formState?.isValid}
type="submit"
className="w-40 disabled:bg-gray-300 flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
Create Post
</button>
</div>
</form>
</div>
</div>
);
}
export default App;
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:
<input
{...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:
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:
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.
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
:
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.
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:
.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:
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:
PostSchema.safeParse({
title: "General Zod of Candor",
subtitle: "Executing...",
content: "Kneel brefore 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: [...] }
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.
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:
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:
Show `` component code
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="min-h-screen flex items-center justify-center w-full dark:bg-gray-950">
<div className="bg-white dark:bg-gray-900 shadow-md rounded-lg px-8 py-6 max-w-md">
<h1 className="text-2xl font-bold text-center mb-4 dark:text-gray-200">
Edit Profile
</h1>
<form
onSubmit={formInstance?.handleSubmit((data) => {
setTimeout(() => {
console.log("data", data);
}, 2000);
})}
>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Username
</label>
<input
{...formInstance?.register("username")}
type="text"
className="shadow-sm rounded-md w-full px-3 py-2 border border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
placeholder="Choose a username"
/>
{formInstance?.formState.errors?.username && (
<span className="text-red-500 text-xs">
{formInstance?.formState.errors?.username?.message as ReactNode}
</span>
)}
</div>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
First Name
</label>
<input
type="text"
className="shadow-sm rounded-md w-full px-3 py-2 border border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
placeholder="First name"
{...formInstance?.register("first_name")}
/>
{formInstance?.formState.errors?.first_name && (
<span className="text-red-500 text-xs">
{
formInstance?.formState.errors?.first_name
?.message as ReactNode
}
</span>
)}
</div>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Last Name
</label>
<input
type="text"
className="shadow-sm rounded-md w-full px-3 py-2 border border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
placeholder="Last name"
{...formInstance?.register("last_name")}
/>
{formInstance?.formState.errors?.last_name && (
<span className="text-red-500 text-xs">
{
formInstance?.formState.errors?.last_name
?.message as ReactNode
}
</span>
)}
</div>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Email
</label>
<input
type="text"
className="shadow-sm rounded-md w-full px-3 py-2 border border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
placeholder="email@exmaple.kr"
{...formInstance?.register("email")}
></input>
{formInstance?.formState.errors?.email && (
<span className="text-red-500 text-xs">
{formInstance?.formState.errors?.email?.message as ReactNode}
</span>
)}
</div>
<div className="flex justify-between">
<button
type="submit"
className="w-40 disabled:bg-gray-300 flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
Save Changes
</button>
</div>
</form>
</div>
</div>
);
}
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:
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");
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:
const formInstance = useForm({
resolver: zodResolver(ProfileSchema),
mode: "onChange",
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:
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
.
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
:
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");
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.
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
(space) into an _
(underscore):
const ProfileSchema = zod.object({
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:
{
"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.