> To simplify the migration process both for users and Zod's ecosystem of associated libraries, Zod 4 is being published alongside Zod 3 as part of the zod@3.25 release. [...] import Zod 4 from the "/v4" subpath
npm is an absolute disaster of a dependency management system. Peer dependencies are so broken that they had to make v4 pretend it's v3.
Author here. I wrote a fairly detailed writeup here[0] for those who are interested in the reasons for this approach.
Ultimately you're right that npm doesn't work well to manage the situation Zod finds itself in. But Zod is subject to a bunch of constraints that virtually no other libraries are subject to. There are dozens or hundreds of libraries that directly import interfaces/classes from "zod" and use them in their own public-facing API.
Since these libraries are directly coupled to Zod, they would need to publish a new major version whenever Zod does. That's ultimately reasonable in isolation, but in Zod's case it would trigger a "version avalanche" would just be painful for everyone involved. Selfishly, I suspect it would result in a huge swath of the ecosystem pinning on v3 forever.
The approach I ended up using is analogous to what Golang does. In essence a given package never publishes new breaking versions: they just add a new subpath when a new breaking release is made. In the TypeScript ecosystem, this means libraries can configure a single peer dependency on zod@^3.25.0 and support both versions simultaneously by importing what they need from "zod/v3" and "zod/v4". It provides a nice opt-in incremental upgrade path for end-users of Zod too.
Are there any plans of doing an actual v4 release and making zod/v4 the default there? Perhaps make simulatenous releases of zod v3 containing a /v4 path and a zod v4 containing a /v3 path? Then converge on just zod v4 with no /v3 from 4.1 onwards.
Yep, at some indeterminate point when I gauge that there's sufficient support for Zod 4 in the ecosystem, I'll publish `zod@4.0.0` to npm. This is detailed in the writeup[0]
Is there any advantage to this approach over publishing a separate "zod4" package? That would be just as opt-in and incremental, not bloat download sizes forever, and make it easier to not accidentally import the wrong thing.
Ecosystem libraries would need to switch from a single peer dependency on Zod to two optional peer dependencies. Despite "optional peer dependencies" technicall being a thing, its functionally impossible for a library to determine which of the two packages your user is actually bringing to the table.
Let's say a library is trying to implement an `acceptSchema` function that can accepts `Zod3Type | Zod4Type`. For starters: those two interfaces won't both be available if Zod 3 and Zod 4 are in separate packages. So that's already a non-starter. And differentiating them at runtime requires knowing which package is installed, which is impossible in the general case (mostly because frontend bundlers generally have no affordance for optional peer dependencies).
Thanks a lot for zod and really looking forward to trying out the new version! Also as a primary Go developer this issue just has been really interesting to read about. Go avoids this issue by just not having peer dependencies and relying on the compiler to not bundle unused code - or just live with huge binaries when it doesn't work out :-)
I am still curious about the `zod4` idea though. Any thoughts on adding an `__api_version` type of property to distinguish on to all of zod's public types? Perhaps it's prohibitive code-size wise but wondering if it's possible somehow. Then downstream libraries could differentiate by doing the property check.
Just wanted to share the idea but the current state seems ok too.
> Peer dependencies are so broken that they had to make v4 pretend it's v3
I'm not sure this is the right conclusion here. I think zod v4 is being included within v3 so consumers can migrate over incrementally. I.e refactor all usages, one by one to `import ... from 'zod/v4'`, and once that's done, upgrade to v4 entirely.
How do you even migrate incrementally? why keep both old and new code together like a Frankeinstein project? Especially on a codebase you own. It's a library, not a platform.
> why keep both old and new code together like a Frankeinstein project?
It seems like Zod is a library that provides schema to your (domain) objects in JS/TS projects, so if you're all-in with this library, it's probably a base-layer for a ton of stuff in the entire codebase.
Now imagine that the codebase is worked on by multiple parties, in different areas, and you're dealing with 100K-1M lines of code. Realistically, you can't coordinate such a huge change with just one "Migrated to Zod 4" commit and call it a day, and you most likely want to do it in pieces.
I'm not convinced publishing V4 as version 3 on npm is the best way of achieving this, but to make that process easier seems to be the goal of that as far as I understand.
If it's part of your core domain, I'd say that one of the reasons you want the feature set and the API to be as stable as possible. And your core domain should probably be isolated from the things that depends on it in such way that only business logic ripples out.
One of the strange things from the NPM/JS culture is the focus to write everything in one language leading to everyone forgetting about modularization and principles like SOLID. We link everything together with strange mechanisms for "DX", which really means "make it easy to write the first version, maintenance be damned".
This comment feels very detached from reality. Migrating core dependencies in large projects is always cumbersome. One universal truth is that you can make complex tasks easy by breaking it up into small pieces. Providing a sensible forward migration path like what Zod does causes 0 harm and makes everyone's life easier. This has absolutely nothing to do with "NPM/JS" culture, whatever that means.
You use Zod to define all the schemas and types used in "your core domain". It is the base layer tool of your business logic and models, and for defining your interfaces. And yes it makes it possible to share all of this between the frontend and backend, which is a tremendous improvement in both correctness and in development productivity.
You don't get anything remotely like this in any other backend frontend combo.
I get the wish to have that, but IMO, it's a flawed approach. The frontend and the backend are generally different domains. Initially, their model may look alike, but they will probably differs during the lifetime of the project as they serve different needs. That's why the shape of the data stored in the DB differs from the one involved in business logic, which differs from the DTOs, which can differs from the objects involved in the presentation.
In simple applications, you can get away with defining a single schema for all, but it helps with keeping in mind that you may have to design a better data shape for some logic.
> The frontend and the backend are generally different domains
It is not a flawed approach, it's an important need for most web apps. Zod is a validation library, the whole purpose of is to define structures that can safely be shared across domains.
The point is that somewhere, you need to chuck bytes over the network and someone needs to receive them.
If the teams working on the two are different, or even if the expertise level is uneven, something like a typed serialization library is a great boon.
At work, I maintain a Haskell-like programming language which spits out JSON representations of charts over OLAP queries. I’m the only one who knows the language extensively, but everyone is expected to do work with the JSON I push. If I serialize something incorrectly, or if someone else mistypes the frontend JSON response definition, we’re in for a world of pain.
Needless to say, I’ll be adding something like Zod to make that situation safer for everyone.
> If the teams working on the two are different, or even if the expertise level is uneven, something like a typed serialization library is a great boon.
You don't even need that, I use typed serialization on both sides when talking to myself. How else do I guarantee the shape of what I send and receive? I want my codebase to scream at me if I ever mess it up.
This is not done for Zod's benefit. It's done for the benefit of libraries that depend on Zod, and the users of those libraries. If a library wants to add "incremental" support for Zod4 (that is, without dropping support for Zod 3 and publishing a new major), they need access to types (and possibly runtime code) from both libraries to do so in a sound way. I detail a number of other approaches for achieving this here[0] and why they ultimately fall short. Ultimately npm wasn't designed for this particular set of circumstances.
Yeah, importing two versions could allow you to pass v3 objects to v4 methods and vice versa, and that seems like an extremely bad idea (if it would even type-check / run).
I feel like people will upvote anything negative. This is not much a limitation of npm, there's nothing intrinsically wrong with npm that lead to this decision, this is more a pragmatic way of allowing progressive change of a library that introduced a lot of braking changes.
Right, you have the same issues to consider when shipping a breaking major version upgrade to a new library in any language/ecosystem.
That said, you do see a cultural difference in node-land vs. many other ecosystems where library maintainers are much quicker to go for the new major version vs. iterating on the existing version and maintaining backward compatibility. I think that's what people are mostly really referring to when they complain about node/npm.
Webpack is a good example—what's it on now, version 5? If it was a Go project, it would probably still be on v1 or maybe v2. While the API arguably does get 'better' with each major version, in practice a lot of the changes are cosmetic and unnecessary, and that's what frustrates people I think. Very few people really care how nice the API of their bundler is. They just want it to work, and to receive security updates etc. Any time you spend on upgrading and converting APIs is basically a waste—it's not adding value for users of your software.
This very much. If I'm using your library, I've already committed to it's architecture and API with all its flaws. And my users don't care for the technology I use. Even if they're not that good, I can build wrapper over the worst aspects and then just focus on building features. New features are nice, but I'm more interested in getting security updates for the current version that I have.
When it's time to go for a refactoring, the trade-off between costs and returns are worth it as you can go for years between those huge refactors.
I might be blindsided by using npm exclusively for years by this point, but what would be a better way to support iteratively migrating from v3 to v4 without having to do it all in one large batch?
As you allude to: your aliased "zod-next" dependency wouldn't be able to satisfy the requirements of any packages with a peer dep on Zod. But this approach has a more fundamental flaw. My goal is to let ecosystem libraries support Zod 3 and Zod 4 simultaneously. There's no reliable way to do that if they aren't in the same package.[0]
One possible solution: Publish a new package with a new name. I've personally been thinking of doing that for some libraries, where I'd commit to never change the public API, and if I figure out a nicer way of doing something, I'd create a new library for it instead, and people who wanna use/maintain the old API can continue to do so.
If they need the new one, they will switch to it. And you have the right to drop support for the old one after a while (hopefully giving everyone the time to migrate).
> If they need the new one, they will switch to it.
I already gave my argument on why this won't happen. People stick to what they know and trust. They don't change because there is some other thing that is supposedly better. It has to be 10x better before people will migrate off one thing to another. If you want to do incremental improvements to a thing, then you'll have to do incremental improvements to that thing.
> They don't change because there is some other thing that is supposedly better. It has to be 10x better before people will migrate off one thing to another.
That's the point, and it makes sense from their perspective, I'd probably do the same as well.
Creating a new library instead of changing the existing one lets people chose between those two approaches. Want the latest and greatest but with API breakage? Use this library. Wanna continue using the current API? Use this library.
Instead, we kind of force the first approach on people, which I personally aren't too much of a fan of.
As I’ve said in other comments. If you’re developing a library, you can commit to its API and do security and optimization fixes and build a new one where you try a new design/approach. Merging the two together is always a bad idea.
This isn't an npm exclusive issue. A dependency having some other transitive deps also depend on an older version is a problem that happens in literally every other ecosystem. If anything, npm gives you more escape hatches by actually allowing you to run multiple versions concurrently if you need to or selectively overriding parts of your transitive dependency graph.
What package management system has a solution to this? Even so called "stable" platforms like Maven deal with this nonsense by publishing new versions under a new namespace (like Apache Commons did from v2 to v3).
We've been using zod 4 beta already with great improvements but due to our huge codebase not being able to handle the required moduleResolution settings, we cannot upgrade...
They could at least also publish it as a major version without the legacy layer
EDIT: I've just seen the reason described here: https://github.com/colinhacks/zod/issues/4371
TLDR: He doesn't want to trigger a "version bump avalanche" across the ecosystem. (Which I believe, wouldn't happen as they could still backport fixes and support the v3 for a time, as they do it right now)
I feel like both Node.js and NPM were initially vibe coded before LLMs existed. Just a quick hack, kind of, that got hugely popular somewhat by accident.
Edit: Thinking about it, that's the origin story of JavaScript as well, so rather fitting.
npm is an absolute disaster of a dependency management system. Peer dependencies are so broken that they had to make v4 pretend it's v3.