- Published on
Form Validation with Zod
- Authors
- Name
- Stackademic Blog
- @StackademicHQ
Zod schema validation
Getting started with zod can be a bit overwhelming, in this article, we'd go over form validation with zod, how to simplify the process and make it easier. We'd be using React, FormData, html form and zod for our validation. If you would like to move on with building typescript generic components, check this article out.
What is zod?
According to the official website for Zod, Zod is a TypeScript-first schema declaration and validation library. The term "schema" refers to any data type, from a simple string to a complex nested object. In simple terms, Zod helps you validate and parse your data to satisfy a certain shape. To get started with zod you'd need a schema, and data. Let's look at defining a basic zod schema
import z from 'zod'
// you can also destructure the items import {string, object, z} from 'zod'
const SimpleSchema = z.object({
name: z.string(),
age: z.number(),
})
After defining our schema, we use it to validate the shape of any data passed into it. If there are errors, it will throw a ZodError
. For a more comprehensive guide on Zod errors and the ZodError type, I recommend reading the documentation.
To safely use our schema, it's best to wrap it in a try-catch block:
try {
const res = SimpleSchema.parse(data)
// do something with the res
} catch (error) {
console.error(error)
// apply better error handling
}
With that out of the way, let's use Zod to validate a signup form. We define a signup form with basic fields below:
const fields = [
{ name: 'fullName', label: 'Full Name', placeholder: 'Enter Full name' },
{ name: 'username', placeholder: 'Enter Username' },
/** We can ue zod to validate that our value is of type email */
{ name: 'email', placeholder: 'Enter email', type: 'email' },
{ name: 'password', placeholder: 'Enter password', type: 'password' },
]
const SignUpForm = () => (
<form className="mx-auto max-w-md space-y-5 py-10">
<h2 className="text-2xl font-bold">Complete the form to sign up</h2>
<div className="space-y-4">
{fields.map(({ name, placeholder, type, label }) => (
<div className="grid" key={name}>
<label htmlFor={name} className="text-sm capitalize">
{label ?? name}
</label>
<input
name={name}
placeholder={placeholder}
type={type ?? 'text'}
className="placeholde r:capitalize border p-3"
id={name}
/>
</div>
))}
</div>
</form>
)
After creating our form, we can define the schema for our form.
const invalid_type_error = 'Invalid type provided for this field'
const required_error = 'This field cannot be blank'
export const SignUpSchema = z.object({
fullName: z.string({ invalid_type_error, required_error }).min(1, 'Value is too short'),
username: z.string({ invalid_type_error, required_error }).min(1, 'Value is too short'),
email: z
.string({ invalid_type_error, required_error })
.email('Please provide a valid email')
.min(1, 'Value is too short'),
password: z.string({ invalid_type_error, required_error }).min(6, 'Password is too short'),
})
A common issue with zod is that an empty string is the valid as type string, the fix for this would be specifying a minimum length of 1 and by default all values are required
const handleSubmit = (e: React.ChangeEvent<HTMLFormElement>) => {
e.preventDefault()
// conversion of the input from FormData to an object
const data = Object.fromEntries(new FormData(e.currentTarget))
try {
const res = SignUpSchema.parse(data)
console.log(res)
} catch (error) {
console.log(error)
}
}
Error Handling
Before we proceed, it's important to note that the following steps work well with one-level data or objects without nesting. This approach is suitable for validating form values since native form values are typically not nested. Now let's dive into the error handling process.
To begin, we define a function called handleOneLevelZodError
, which handles error messages generated by Zod validation. It takes a ZodError<unknown>
object as an argument.
// @argument ErrorType : if you need to assert your error type, just pass it as a generics via asserting with the as keyword
const handleOneLevelZodError = ({ issues }: ZodError<unknown>) => {
const formData: Record<string, string> = {};
// - line of code should be true if the schema is not an object
// - This line is completely optional
if (issues.length === 1 && issues[0].path.length < 1)
return issues[0].message;
issues.forEach(({ path, message }) => {
formData[path.join('-')] = message;
});
return formData;
};
export default handleOneLevelZodError
The handleOneLevelZodError
function initializes an empty object called errorMessages
. It then iterates through the issues
array of the ZodError
object, extracting the path
and message
for each issue. The path
is joined with a hyphen (-
) and used as the key in the errorMessages
object, with the corresponding error message as the value.
If the ZodError
contains only one issue and the path
length is less than 1, indicating that the schema is not an object, the function returns the error message directly. Please note that this line is optional and can be removed if it doesn't apply to your specific use case.
Finally, the function returns the errorMessages
object containing the error messages. You can utilize this object to display the error messages to the user or handle them in any other appropriate manner within your application.
By importing and using the handleOneLevelZodError
function, you'll be able to handle error messages generated during Zod validation effectively.
handleOneLevelZodError
allows us to validate schemas of type zod object
or zod primitives
. The next step is to define a reusable function that extracts the repetitive try-catch block into a well-typed function
type ZObjectType = ZodType<Record<string | number, unknown>>
type ZodParams<T extends ZObjectType> = {
onSuccess(data: T['_output']): void
onError(error: Partial<Record<keyof T['_output'], string>>): void
data: Record<string, unknown>
schema: T
}
export type ValidationError<T extends ZObjectType> = Partial<Record<keyof T['_output'], string>>
export const handleZodValidation = <T extends ZObjectType>(params: ZodParams<T>) => {
const { data, onError, onSuccess, schema } = params
try {
const res = schema.parse(data)
onSuccess(res)
} catch (error) {
if (error instanceof ZodError) {
const formattedErr = handleOneLevelZodError(error)
onError(formattedErr as Record<keyof T['_output'], string>)
} else {
throw new Error(String(error))
}
}
}
The code defines a type ZObjectType
, which represents a Zod schema type narrowed to objects, so we can use the keys for generating our error messages. Additionally, the ZodParams
type defines the expected parameters for the handleZodValidation
function, including onSuccess
and onError
callbacks, input data
, and the Zod schema
to validate against.
The ValidationError
type is defined to represent partial error messages for the validated schema. It uses keyof T["_output"]
to ensure the error object keys align with the expected output of the Zod schema.
The handleZodValidation
function takes in the params
object and attempts to parse the input data
using the provided schema
. If parsing succeeds, the onSuccess
callback is invoked with the parsed result. If a ZodError
is thrown during parsing, the handleOneLevelZodError
function is called to format the error messages. The onError
callback is then invoked with the formatted error object. If any other type of error occurs, it is thrown to be caught and handled elsewhere.
By utilizing the handleZodValidation
function, you can simplify your validation code and handle success and error cases more effectively.
const [errors, setErrors] = useState<ValidationError<typeof SignUpSchema>>({})
const handleSubmit = (e: React.ChangeEvent<HTMLFormElement>) => {
e.preventDefault()
/** conversion of the input from FormData to an object */
const data = Object.fromEntries(new FormData(e.currentTarget))
handleZodValidation({
onError: setErrors,
data: data,
onSuccess: (res) => {
/**
* res is of type:
* { fullName: string; username: string; email: string; password: string; }
*/
console.log(res)
},
schema: SignUpSchema,
})
}
In conclusion, Zod proves to be a powerful tool for form validation, especially when combined with TypeScript. By leveraging Zod's schema declaration and validation capabilities, developers can ensure that the data submitted through forms adheres to the desired structure and meets specific validation rules.
Thank you for making it all the way here, if you are interested in creating a typescript generic component, I've got something for you here.