Lessons Learned Migrating a Large Production Codebase from Zod v3 to v4

Search for a command to run...

No comments yet. Be the first to comment.
Linked lists are a linear data structure consisting of nodes that are not stored contiguously (right next to each other) in memory. Each node of the linked list consists of data and a next pointer. Since (unlike the array) a linked list is not stored...

To start off our computer science fundamentals series, we will cover complexity analysis and Big O. Prior to understanding data structures and algorithms, it's important to understand how code is analyzed for efficiency in software engineering. This ...

When I started my React journey back in 2019, hooks had just been introduced a couple of months earlier in February of that year. After coming across multiple sources vouching for the ease and simplicity of functional components compared to class-bas...

A frontend spike revealed that email validation logic between the backend and frontend was not consistent. The backend depended on a large Mailman regex with additional length checks, while the frontend validation was less restrictive.
As part of improving consistency, maintainability, and centralizing validation logic across the repo, Zod v4 presented a strong opportunity to modernize how validation is handled across the repository.
The goal was to align frontend validation rules with backend requirements while also reducing technical debt. Migrating to standardized Zod-based validation allowed us to:
Prevent email addresses accepted on the frontend but rejected by the backend
Update the email validation regex on the frontend to match backend validation
Maintain consistent validation behavior across the app
Reduce manual regex maintenance and duplication
Centralize email address validation into a reusable shared schema
While the migration was initially motivated by email validation, it quickly became clear that upgrading Zod would provide long-term improvements in developer experience, type safety, and validation reliability.
Like many large-scale dependency upgrades, the Zod migration uncovered several prerequisite upgrades that needed to happen first.
When I initially took on on the the project, I discovered our TypeScript version was still on v4.9 while Zod v4 required TypeScript v5.5 or higher. This meant the migration could not happen without first upgrading TypeScript across the repository.
After upgrading TypeScript, additional compatibility issues surfaced with React Hook Form (RHF) and @hookform/resolvers, both required upgrades to support the newer ecosystem.
Adding a bit more complication, our Design System team maintained shared form components that depended on RHF. Their components were running on an older version than what Zod v4 needed to work with, which created a dependency mismatch that prevented a straightforward upgrade.
To resolve this, I collaborated with the Design System team by outlining:
Why upgrading RHF was necessary for Zod v4 compatibility
How mismatch impacted validation and form integrations
The importance of modernizing our ecosystem
Because these shared components were consumed across multiple applications, the upgrade required careful coordination and testing, which extended the migration timeline.
Once the Design System updates were completed and deployed, I was able to successfully install and integrate Zod v4 without additional compatibility issues.
Once Zod v4 was installed, I quickly realized how loosely validated Zod v3 was compared to v4. Running TypeScript after the upgrade surfaced 80+ type errors across the codebase. While this initially felt overwhelming, a few of the errors highlighted areas where validation logic had been loosely defined or relied on deprecated APIs that included simple updates such as:
Replacing message with error
Replacing invalid_type_error and required_error with error
Replacing import { schema } from “zod” from import { z } from “zod”
Beyond syntax updates, Zod v4 introduced noticeably stricter type enforcement. In several files, schemas that compiled successfully now failed due to mismatches between expected and actual data shapes. The stricter checks surfaced validation issues such as:
Optional values being treated as required
Inconsistent handling of nullable vs optional fields
Incorrect union definitions
Even found a case where a value was spelled incorrectly but still passed in v3!
Zod v4 also refined how schemas are composed and extended. Several chained validation patterns that worked in v3 required restructuring to align with the new composition behavior.
In particular, complex validation scenarios involving:
.refine()
.superRefine()
Schema merging and extension
transform and coerce changed to type unknown
Because Zod was tightly integrated with React Hook Form, upgrading Zod required verifying compatibility with the latest resolver versions. Schema changes occasionally affected how form errors were mapped and surfaced in UI components.
In some cases, validation behavior subtly changed due to stricter typing or updated resolver expectations, requiring regression testing across form-heavy workflows.
Large dependency migrations rarely fail because of the library upgrade itself. They expose inconsistencies, technical debt, and architectural drift that accumulate over time.
Zod v4 did exactly that for our codebase. While the migration required significant effort, it resulted in stronger type guarantees, more consistent validation behavior, and a cleaner foundation for future development.