Regarding the versioning: I wrote a fairly detailed writeup here[0] for those who are interested in the reasons for this approach.
Ultimately npm is not designed to handle the situation Zod finds itself in. Zod is subject to a bunch of constraints that virtually no other libraries are subject to. Namely, the 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.
> 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 [email protected] 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.
> 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.
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.
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?
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.
I'm curious if anyone here can answer a question I've wondered about for a long time. I've heard Zod might be in the right ballpark, but from reading the documentation, I'm not sure how I would go about it.
Say I have a type returned by the server that might have more sophisticated types than the server API can represent. For instance, api/:postid/author returns a User, but it could either be a normal User or an anonymous User, in which case fields like `username`, `location`, etc come back null. So in this case I might want to use a discriminated union to represent my User object. And other objects coming back from other endpoints might also need some type alterations done to them as well. For instance, a User might sometimes have Post[] on them, and if the Post is from a moderator, it might have special attributes, etc - another discriminated union.
In the past, I've written functions like normalizeUser() and normalizePost() to solve this, but this quickly becomes really messy. Since different endpoints return different subsets of the User/Post model, I would end up writing like 5 different versions of normalizePost for each endpoint, which seems like a mess.
You can define passthrough behavior if there are a bunch of special attributes for a moderator but you don't want to list/check them all.
With different methods that have different schema- If they share part of the same schema with alterations, you can define an object for the shared part and create objects that contain the shared object schema with additional fields.
If you have a lot of different possibilities, it will be messy, but it sounds like it already is for you, so validators could still at least validate the messines.
In an ideal world you'd have one source of truth for what the shape of a User could be (which may well be a discriminated union of User and AnonymousUser or similar).
Without fullstack TS this could look something like: (for a Python backend) Pydantic models+union for the various shapes of `User`, and then OpenAPI/GraphQL schema generation+codegen for the TS client.
Zod is a lot better than some of the alternative solutions I've seen. That said, the need for this sort of explicit validation always felt like a failure of where modern web development has taken us. It's so frustrating how full-stack development requires so many ways of describing the same shapes: JS input validation, Swagger for API definition, server-side input validation, ORM for schema conformance, and TypeScript often requiring separate definitions on server and client side. It's so tedious.
TypeScript's insistence on being a static checking/compile time only system is such a sad wound to the whole ecosystem. I don't want TypeScript to be a runtime checker, but I want it to export useful usable type data for classes, functions, objects. TypeScript feels like the best source of truth we have, but instead so many of the attempts to reflect on what we have are all going it alone, having to define their own model and their own builders for describing what stuff is. You've mentioned 5 major areas where reflection is a core part of the mission, and each of these have multiple implementations.
There is the old TypeScript type emitter, reflect-metadata, which would provide some type information at runtime, but it targets very old decorator and metadata specifications, not the current version. I don't super know how accurate or complete it's model really is, how closely it writes down what typescript knows versus how much it defines its own model to export to. https://www.npmjs.com/package/reflect-metadata
We are maybe at the cusp of some unifying, some coming together, albeit not via typescript at this time, still as a separate layer. The Standard Schema project has support from I dare say most of the top validation libraries. But extending this to API definitions, ORM tools is in extremely early stages.
https://github.com/standard-schema/standard-schema?tab=readm...
But that’s the whole point of something like this. You do it once and dynamically generate everything else downstream. So change it once in the zod schema and it propagates with type checking through your entire app.
You shouldn’t typically need to redefine your data all over the places. There are zillions of converters for everything to zod, and zod to everything.
If you already have JSON schema/swagger schemas, generate zod from em. If you use a typescript orm, I bet you $10 there’s a zod generator for it.
Honestly zod has gotten so popular that for me, it’s the unifying schema definition that everything else in the stack can rely on. And since directly defines the types, devs actually keep it up to date (swagger docs at my company are ALWAYS lagging behind changes)
> It's so frustrating how full-stack development requires so many ways of describing the same shapes: JS input validation, Swagger for API definition, server-side input validation, ORM for schema conformance, and TypeScript often requiring separate definitions on server and client side. It's so tedious.
Many of the same people who complain about how complicated modern web dev is would also shudder at the suggestion to just replace all those things with TypeScript (and Zod, if that's your TS schema definition and validation library of choice).
How else would you do it? You could use something like trpc for full end to end type safety but that requires using TypeScript on both the frontend and backend (which not everyone does) and also locking yourself into only the web platform (so no mobile, etc).
Yes there should at least be a way to consolidate so that we can agree on format to treat as the source of truth that we can generate the other formats from.
Zod 4 looks good but even with their latest improvements, ArkType is still an order of magnitude faster. Sometimes for the sake of backward and syntax compatibility, it is difficult to make something much faster than a fully greenfield newer library. We recently did an analysis of all these types of tools for our project and decided to go with ArkType for partially this reason, the other was TypeScript ergonomics felt nicer.
I was looking at all their speed metrics, and can you explain to me where the speed makes a difference?
We only use zod to validate forms, so I keep thinking "how does this matter?" Are people maybe using it to validate high throughput API input messages or something like that, where performance may matter more?
Interesting, that one didn't even appear in my research. I was specifically looking for typescript ergonomics too. Probably still not going to switch away from Zod, though.
Congratulations to the Zod team on the new release. At the risk of sounding overtly negative, I can't help but shudder when I think about the number of breaking changes outlined in the migration guide. For projects that rely heavily on Zod, it feels like a daunting task ahead—one that will demand a lot of developer attention and time to navigate. Having maintained a few frontend projects that are 4-5 years old at work, I really empathize with them.
In my experience, large React projects often depend on a multitude of libraries, and when each one rolls out substantial changes—sometimes with barely any documentation—it can quickly become overwhelming. This is honestly one of my least favorite aspects of working with JavaScript. It just feels like a constant uphill battle to keep everything in sync and functioning smoothly.
I am 100% in agreement here. I operate a couple large Next.js apps and in the last year we've had to deal with Next.js 14 -> 15 which introduced a ton of breaking cache changes, Next.js pages -> app router, React 18 -> 19, Eslint 8 -> 9, and Tailwind 3 -> 4.
It's honestly been a nightmare, and I wish I had just built in Django instead. The Tailwind 3 -> 4 migration was probably among the most painful, which I was not expecting.
that's why they're taking the dual-availability approach, with a separate 'mini' edition. it's easy to perform a progressive migration without messing with the package manager.
consumers uninterested in the 'mini' edition don't have to bother with that part.
but, the benefits of the 'mini' edition are so drastic for tree-shaking that it was driving development of alternatives - zod had to either do it (and benefit), or deprecate.
As a full stack dev that runs their own SaaS with thousands of users in production, sometimes I get a very pleasant reminder of how many problems I am blissfully unaware of just because I decided not to build an SPA or use JS frontend frameworks. It never occurred to me that I need such a thing as Zod/ArkType - first time hearing of it, and I have no use for it.
It boggles my mind how much effort and complexity and tooling goes into building an SPA. Entire classes of problems simply don't exist if you choose not to build an SPA. Meanwhile, I use the browser as designed: with full page reloads, backend development only, and occasional reactivity using a backend-only framework like Laravel Livewire. Everything is so simple: from access control to validation to state management. And yes, my app is fast, reactive, modern, SEO friendly, and serves thousands of users in production.
I am not an expert here but I had a thought that JSON-Schema might be a good choice because since it's schema based, i can implement the validators in Non-Typescript languages too.
https://ajv.js.org is one such JSON Schema library. How does zod compare to this?
I tested early versions of zod v4 and liked the new API, but was very concerned about what will be the migration path. I was even going to suggest to publish under a new package name.
But the author's approach is ingenious. It allows for someone like me to start adopting v4 immediately without waiting for every dependency to update.
I just got started working Zod into a new project. This could not have happened at a better time. I would have needed to change so much ot migrate to v4 based on what I'm seeing.
We're currently evaluating both Zod and ArkType as a replacement for earlier JSON schema validations. The thing I can't get over with Zod is the syntax is _so_ different from defining TS types.
With yet another exciting new release of something reaching the top of HN, I would just like to urge devs to put a description of the project they're actually releasing and a link to the page describing the project in the release announcement.
These announcements could be a valuable touchpoint for you to reach a whole new audience, but I can't remember a single one that starts with something like "exciting new release of NAME, the X that does Y for users of Z. Check out the project home page at https:// for more."
Quite often, the release announcement is a dead end that can't even take me to the project! In this case, the only link is a tiny octocat in the lower left-hand corner, AFAICS.
The versioning approach here is a really interesting compromise — especially in the npm ecosystem where breaking changes ripple out so painfully through transitive dependencies. From a developer ergonomics standpoint though, the shift to zod/v4 imports will definitely create some friction. For teams with IDEs auto-importing from 'zod' and linters enforcing import styles, it might introduce subtle DX issues until workflows are adjusted.
That said, the strategy does seem to prioritize ecosystem stability over short-term convenience, which is fair. Would love to see better tooling (maybe even IDE plugins or codemods) to help projects transition cleanly. Really appreciate the thoughtful design behind all of this.
One decision that seems dubious to me is the zod/v4-mini import. I suspect that this will actually increase bundle sizes across the ecosystem. The docs specifically say "zod/v4 is still recommended for the majority of use cases", so application developers will use it. Library authors will think to themselves, "but I want to enable my library to be used by those with uncommonly strict bundle size requirements", and so will use that. The net result is that both zod/v4 and zod/v4-mini will be included in the application bundle.
I guess this could be mostly avoided if zod/v4 actually a wrapper around zod/v4-mini. Is that the case?
But how is the below possible - doesn't it need to include most of the TypeScript compiler? Does it compile the type definitions to some kind of validation structure (like how a compiled regex works) and then use just that?
- Zero external dependencies
- Works in Node.js and all modern browsers
- Tiny: 2kb core bundle (gzipped)
The TypeScript compiler is only needed during development, the compiled JavaScript code contains no TypeScript-specific logic. Zod’s validation is entirely JavaScript-based, relying on simple checks (e.g., typeof, regexes, comparisons).
Also, the 2kb bundle just for the zod/v4-mini package, the full zod/v4 package is quite large.
I think Zod uses JIT compilation via `new Function`, rather than including the entire TypeScript compiler. This method allows for concise validation logic, executing only what’s necessary at runtime.
The library pretty much remained unusable garbage without documentation comments. I have no idea why it became so popular and why this release is named "stable" when there is even no 4.0.0.
[+] [-] colinmcd|9 months ago|reply
Regarding the versioning: I wrote a fairly detailed writeup here[0] for those who are interested in the reasons for this approach.
Ultimately npm is not designed to handle the situation Zod finds itself in. Zod is subject to a bunch of constraints that virtually no other libraries are subject to. Namely, the 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.
[0] https://github.com/colinhacks/zod/issues/4371
[+] [-] rafram|9 months ago|reply
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.
[+] [-] colinmcd|9 months ago|reply
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.
[0] https://github.com/colinhacks/zod/issues/4371
[+] [-] gejose|9 months ago|reply
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.
[+] [-] Trufa|9 months ago|reply
[+] [-] herrkanin|9 months ago|reply
[+] [-] bilalq|9 months ago|reply
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).
[+] [-] aiiizzz|9 months ago|reply
[+] [-] derN3rd|9 months ago|reply
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)
[+] [-] swyx|9 months ago|reply
[+] [-] fhd2|9 months ago|reply
Edit: Thinking about it, that's the origin story of JavaScript as well, so rather fitting.
[+] [-] johnfn|9 months ago|reply
Say I have a type returned by the server that might have more sophisticated types than the server API can represent. For instance, api/:postid/author returns a User, but it could either be a normal User or an anonymous User, in which case fields like `username`, `location`, etc come back null. So in this case I might want to use a discriminated union to represent my User object. And other objects coming back from other endpoints might also need some type alterations done to them as well. For instance, a User might sometimes have Post[] on them, and if the Post is from a moderator, it might have special attributes, etc - another discriminated union.
In the past, I've written functions like normalizeUser() and normalizePost() to solve this, but this quickly becomes really messy. Since different endpoints return different subsets of the User/Post model, I would end up writing like 5 different versions of normalizePost for each endpoint, which seems like a mess.
How do people solve this problem?
[+] [-] stephen_hn|9 months ago|reply
const MyResult = z.discriminatedUnion("status", [ z.object({ status: z.literal("success"), data: z.string() }), z.object({ status: z.literal("failed"), error: z.string() }), ]);
You can define passthrough behavior if there are a bunch of special attributes for a moderator but you don't want to list/check them all.
With different methods that have different schema- If they share part of the same schema with alterations, you can define an object for the shared part and create objects that contain the shared object schema with additional fields.
If you have a lot of different possibilities, it will be messy, but it sounds like it already is for you, so validators could still at least validate the messines.
[+] [-] probabletrain|9 months ago|reply
Without fullstack TS this could look something like: (for a Python backend) Pydantic models+union for the various shapes of `User`, and then OpenAPI/GraphQL schema generation+codegen for the TS client.
[+] [-] causal|9 months ago|reply
[+] [-] jauntywundrkind|9 months ago|reply
There is the old TypeScript type emitter, reflect-metadata, which would provide some type information at runtime, but it targets very old decorator and metadata specifications, not the current version. I don't super know how accurate or complete it's model really is, how closely it writes down what typescript knows versus how much it defines its own model to export to. https://www.npmjs.com/package/reflect-metadata
We are maybe at the cusp of some unifying, some coming together, albeit not via typescript at this time, still as a separate layer. The Standard Schema project has support from I dare say most of the top validation libraries. But extending this to API definitions, ORM tools is in extremely early stages. https://github.com/standard-schema/standard-schema?tab=readm...
[+] [-] koolba|9 months ago|reply
The zod schema becomes the source of truth.
[+] [-] chamomeal|9 months ago|reply
If you already have JSON schema/swagger schemas, generate zod from em. If you use a typescript orm, I bet you $10 there’s a zod generator for it.
Honestly zod has gotten so popular that for me, it’s the unifying schema definition that everything else in the stack can rely on. And since directly defines the types, devs actually keep it up to date (swagger docs at my company are ALWAYS lagging behind changes)
[+] [-] 90s_dev|9 months ago|reply
The only time status quo is fine is when it's for clients and employers who just want stuff done and don't care how.
In that context, all these unfortunate layers of complexity at least mean more billable hours.
[+] [-] tshaddox|9 months ago|reply
Many of the same people who complain about how complicated modern web dev is would also shudder at the suggestion to just replace all those things with TypeScript (and Zod, if that's your TS schema definition and validation library of choice).
[+] [-] blackoil|9 months ago|reply
[+] [-] satvikpendem|9 months ago|reply
[+] [-] worldsayshi|9 months ago|reply
[+] [-] satvikpendem|9 months ago|reply
[+] [-] atonse|9 months ago|reply
We only use zod to validate forms, so I keep thinking "how does this matter?" Are people maybe using it to validate high throughput API input messages or something like that, where performance may matter more?
[+] [-] sroussey|9 months ago|reply
[+] [-] andrewflnr|9 months ago|reply
[+] [-] dangoodmanUT|9 months ago|reply
[+] [-] 8s2ngy|9 months ago|reply
In my experience, large React projects often depend on a multitude of libraries, and when each one rolls out substantial changes—sometimes with barely any documentation—it can quickly become overwhelming. This is honestly one of my least favorite aspects of working with JavaScript. It just feels like a constant uphill battle to keep everything in sync and functioning smoothly.
[+] [-] nicksergeant|9 months ago|reply
It's honestly been a nightmare, and I wish I had just built in Django instead. The Tailwind 3 -> 4 migration was probably among the most painful, which I was not expecting.
[+] [-] ruined|9 months ago|reply
consumers uninterested in the 'mini' edition don't have to bother with that part.
but, the benefits of the 'mini' edition are so drastic for tree-shaking that it was driving development of alternatives - zod had to either do it (and benefit), or deprecate.
[+] [-] koakuma-chan|9 months ago|reply
Or just use an LLM.
[+] [-] _1tem|9 months ago|reply
It boggles my mind how much effort and complexity and tooling goes into building an SPA. Entire classes of problems simply don't exist if you choose not to build an SPA. Meanwhile, I use the browser as designed: with full page reloads, backend development only, and occasional reactivity using a backend-only framework like Laravel Livewire. Everything is so simple: from access control to validation to state management. And yes, my app is fast, reactive, modern, SEO friendly, and serves thousands of users in production.
[+] [-] varbhat|9 months ago|reply
https://ajv.js.org is one such JSON Schema library. How does zod compare to this?
[+] [-] punkpeye|9 months ago|reply
I tested early versions of zod v4 and liked the new API, but was very concerned about what will be the migration path. I was even going to suggest to publish under a new package name.
But the author's approach is ingenious. It allows for someone like me to start adopting v4 immediately without waiting for every dependency to update.
Well done!
[+] [-] labadal|9 months ago|reply
[+] [-] neilpa|9 months ago|reply
Are there reasons to go with Zod over ArkType?
[+] [-] indigovole|9 months ago|reply
These announcements could be a valuable touchpoint for you to reach a whole new audience, but I can't remember a single one that starts with something like "exciting new release of NAME, the X that does Y for users of Z. Check out the project home page at https:// for more."
Quite often, the release announcement is a dead end that can't even take me to the project! In this case, the only link is a tiny octocat in the lower left-hand corner, AFAICS.
[+] [-] mac9|9 months ago|reply
[+] [-] sroussey|9 months ago|reply
Last I looked, the nice thing about TypeBox was that is _was_ JsonSchema just typed which was nice for interoperability.
[+] [-] 90s_dev|9 months ago|reply
Is there a comparison guide? Never heard of this before, but I used io-ts and ajv.
[+] [-] pipallweek|9 months ago|reply
That said, the strategy does seem to prioritize ecosystem stability over short-term convenience, which is fair. Would love to see better tooling (maybe even IDE plugins or codemods) to help projects transition cleanly. Really appreciate the thoughtful design behind all of this.
[+] [-] aduffy|9 months ago|reply
[+] [-] TheFlashBold|9 months ago|reply
[+] [-] CGamesPlay|9 months ago|reply
I guess this could be mostly avoided if zod/v4 actually a wrapper around zod/v4-mini. Is that the case?
[+] [-] LinusU|9 months ago|reply
ref: https://zod.dev/library-authors?id=how-to-support-zod-and-zo...
[+] [-] jasonthorsness|9 months ago|reply
[+] [-] _tqr3|9 months ago|reply
Also, the 2kb bundle just for the zod/v4-mini package, the full zod/v4 package is quite large.
[+] [-] Aeyxen|9 months ago|reply
[+] [-] enbugger|9 months ago|reply