Skip to content

Conversation

@theshadow27
Copy link

@theshadow27 theshadow27 commented Jun 15, 2025

PR: Add Zod v4 Support to TypeMap

TL;DR

This PR introduces first-class support for Zod v4 schemas while preserving full compatibility with existing Zod v3 functionality.
All core converters, guards, compile paths, and tests now understand v4.
Remaining work is limited to a few low-level API deltas and edge-case tests (see Future Work).


Motivation

Zod v4 breaks API parity with v3 (e.g. top-level string validators, new error maps, def vs _def, strict/loose objects).
Consumers who have already migrated to v4 need TypeMap to round-trip those schemas to and from Syntax, TypeBox, and Valibot. This PR delivers that capability without forcing v3 users to upgrade.


High-Level Changes

Area Summary
Core New /src/zod4/ namespace with a drop-in Zod4 builder and bidirectional converters.
Guard Added IsZod4 detection (checks ~standard.vendor === 'zod' and def/meta markers). Refactored common vendor logic.
Compile API No change required—existing TypeBox fallback already handles v4 via new converters.
Public Exports index.ts re-export now includes all Zod4 artefacts (TZod4, Zod4FromTypeBox, …).
Tests typebox-from-zod4 and zod4-from-typebox round-trip tests
zod-detection-test covering v3 vs v4 heuristics
Docs README and examples updated.
Package.json Declares zod as a peer with ^3.25.0 (the first tag that contains the /v4 export path). No new dependencies added, beyond the version bump.

Detailed Implementation Notes

  1. Core Builder (/src/zod4/zod4.ts)

    • Mirrors the v3 builder but imports import { z } from 'zod/v4' and exposes Zod4/TZod4.
  2. Converter Matrix

    ┌───────────────┐        ┌─────────────┐
    │   Syntax      │◄──────►│   Zod v4    │
    ├───────────────┤        ├─────────────┤
    │   TypeBox     │◄──────►│   Zod v4    │
    ├───────────────┤        ├─────────────┤
    │   Valibot     │◄──────►│   Zod v4    │
    ├───────────────┤        ├─────────────┤
    │   Zod v3      │◄──────►│   Zod v4    │
    ├───────────────┤        ├─────────────┤
    │   Zod v4      │◄──────►│   Zod v4    │
    └───────────────┘        └─────────────┘
    
    • All ten paths (five each direction) implemented under /src/{syntax,typebox,valibot}/…-from-zod4.ts and /src/zod4/zod4-from-*.ts.
  3. Version Detection

    // v3: has _def, lacks meta
    // v4: has  def, has  meta

    Ensures mixed-version projects resolve the correct converter without false positives.

  4. Top-Level String & IP Validators

    • Added explicit handling so that z.email() etc. map to their v3 equivalents for cross-conversion.
    • All 26 string formats have been added to the FormatRegistry:

    base64, base64url, cidrv4, cidrv6, cuid, cuid2, date,
    datetime, duration, e164, email, emoji, guid, ipv4, ipv6,
    json_string, jwt, ksuid, lowercase, nanoid, time, ulid, uppercase, url, uuid, xid


Backward Compatibility

  • No breaking API changes.
  • v3 users continue to rely on import * as z from 'zod' paths; detection logic chooses v3 converters.

Performance

Re-ran the benchmarks with z4 support. Key takeaways:

  • Zod v4 native validation is noticeably faster than Zod v3, closing much of the performance gap.
  • TypeMap’s Zod v4 integration performs comparably to native usage, adding only ~14% overhead in these tests.
  • Compiled TypeBox validators remain an order-of-magnitude faster across all libraries.

Future Work (out of scope)

  1. API deltas

    • Number safe-integer restrictions
    • z.strictObject / z.looseObject
    • z.nonoptional() / z.prefault()
  2. Edge-Case Tests

    • Parameterised schemas (test/parameters-zod4.ts)
    • Nested/recursive coercions
  3. E2E Matrix

    • Mixed v3/v4 apps in CI matrix

Checklist

  • Core implementation
  • Unit tests pass (npm test)
  • README & examples updated
  • Open follow-up issues for outstanding API deltas
  • Bump minor version in package.json after merge

Ready for review. Please focus on:

  • Correctness of version detection (IsZod4)
  • Completeness of converter coverage
  • Any unforeseen side-effects on existing v3 workflows

cc: @typemap-maintainers @sinclairzx81 @colinhacks

@theshadow27 theshadow27 mentioned this pull request Jun 14, 2025
@sinclairzx81 sinclairzx81 self-assigned this Jun 15, 2025
@sinclairzx81 sinclairzx81 self-requested a review June 15, 2025 04:02
@sinclairzx81
Copy link
Owner

sinclairzx81 commented Jun 15, 2025

@theshadow27 Hi, this PR is a amazing work, thank you!

So going through, everything looks good to me, again excellent work! I have left a couple of comments on the typebox-from-zod4 implementation, mostly just to try get the type / runtime logic synced up a bit more.

TypeMap leans heavily on having runtime and type mapping logic aligned as closely as possible (they should be identical if possible). As a general rule, runtime logic should only be implemented if that same logic can be expressed in the type system (where the type system implementation usually dictates the runtime implementation).

Just have a quick look a see if it's possible to express this logic using === checks only.


This PR looks great and would be happy to merge in with the above changes.

Do you think it would it be worthwhile including a z3 -> z4 and z4 -> z3 mapping? It's a little bit strange but technically possible via z3 -> typebox -> z4. This might prove helpful for implementations migrating from z3 or vice versa.

Let me know your thoughts on the above.

Again, nice work, this one will be good to merge :)
S

@theshadow27
Copy link
Author

theshadow27 commented Jun 15, 2025

Hi @sinclairzx81 — thanks for the quick turnaround and the detailed notes.
A few points:

So going through, everything looks good to me, again excellent work! I have left a couple of comments on the typebox-from-zod4 implementation, mostly just to try get the type / runtime logic synced up a bit more.

I don’t see inline comments in the GitHub UI yet—maybe the review wasn’t submitted? Assuming you’re referring to the string-format converter, yeah, it didn't sit well, but it was 2AM 🤣 . I’ve since re-worked it to FromStringLike with a review of every possible type that zod/v4 supports.

The only wrinkle is that Zod v4 holds sets of regexes for certain formats, while I could only find a single pattern prop in the TypeBox String opts - which makes since, since TypeBox aims to be faithful to JSONSchema.

If TypeBox has a recommended pattern-combining helper I missed, let me know! The only other option I can think of is to return it as an intersection (allOf / A&B) type. Since TypeScript doesn't really have RegExp support, the closest parallel I can think of would be template types, which would turn into an intersection?

image

Regardless, it's worth noting that the assumption of "one pattern" string was also present in the original zod implementation, and it lead to some undetected bugs there. I added some extra tests checking this to both test/typebox-from-zod.ts and test/typebox-from-zod4.ts, and sure enough, the original implementation fails because of this. To get it to build, I'm bypassing these tests by default, but you can get to them at any time by running

EXTRA_TESTING=true npm run test

v3 with extra tests (9 failures)

String Validation Behavior
      Basic String Checks
         Should validate startsWith correctly
         Should validate endsWith correctly
        1) Should validate includes correctly 🚨
        2) Should validate regex patterns correctly 🚨
        Combined String Checks
           Should validate startsWith + endsWith correctly
          3) Should validate startsWith + includes correctly 🚨
          4) Should validate endsWith + includes correctly 🚨
          5) Should validate startsWith + endsWith + includes correctly 🚨
        Special Case Combinations
           Should validate ipv4 with startsWith correctly
          6) Should validate regex with startsWith correctly 🚨
           Should validate email with endsWith correctly
        Edge Cases
           Should handle empty string checks correctly
          7) Should handle special regex characters in string checks 🚨
          8) Should handle conflicting constraints 🚨
      Format and Constraint Combinations
         Should combine format with length constraints
         Should combine format with pattern constraints
        9) Should handle multiple constraints on formats  🚨

V4 with extra tests (3 failures)

the little regex hack I put together does make a number of these pass, but not perfect...

String Validation Behavior
    Basic String Checks
       Should validate startsWith correctly
       Should validate endsWith correctly
      10) Should validate includes correctly 🚨
      11) Should validate regex patterns correctly 🚨
       Should validate lowercase correctly
       Should validate uppercase correctly
    Combined String Checks
       Should validate startsWith + endsWith correctly
       Should validate startsWith + includes correctly
       Should validate endsWith + includes correctly
       Should validate startsWith + endsWith + includes correctly
    Special Case Combinations
       Should validate ipv4 with startsWith correctly
      12) Should validate regex with startsWith correctly 🚨
       Should validate email with endsWith correctly
    Edge Cases
       Should handle empty string checks correctly
       Should handle special regex characters in string checks
       Should handle conflicting constraints

  Format and Constraint Combinations
     Should combine format with length constraints
     Should combine format with pattern constraints
     Should handle multiple constraints on formats

I don't think there's actually a way to accurately and arbitrarily combine regex strings into a single pattern (as required by TypeBox and JsonSchema). So either, we leave it alone with a warning, or look into automatic intersections. Let me know what you think!


Do you think it would it be worthwhile including a z3 -> z4 and z4 -> z3 mapping? It's a little bit strange but technically possible via z3 -> typebox -> z4. This might prove helpful for implementations migrating from z3 or vice versa.

I started with the goal of complete symmetry (and do very much admire the beauty of such things), but the divergence between v3 and v4 (especially around _def/def, top-level validators, and the new error/regex model) seemed to be turning into a minefield/science project.

I agree there’s real value in one-hop z3 ⇄ z4 mappings for mixed-version projects and imports from different libraries, but this really deserves a focused effort (and test matrix) rather than riding on the baseline v4 integration. Before going down that route, I'd want to look at other converters, and probably solicit input from Colin on if there are existing plans... Any cursory effort will become a bug-laden trap, so I set it as explicitly out of scope (which is why it was absent).

However, I think it would be possible to stub in a valibot-from-zod style 2-hop adapter, i.e.

zod-from-zod4 ::= zod4 -> TypeBox -> zod
zod4-from-zod ::= zod -> TypeBox -> zod4

That way, in the future, a direct one-hop conversion would not change the API. Are you OK with a quick stub and a follow-on for the direct conversion? I’m happy to do the stub and brainstorm on the direct conversion in a different thread, if that is ok with you.

Appreciate the feedback—and thanks for maintaining such a well-structured codebase. 🙌

@theshadow27
Copy link
Author

theshadow27 commented Jun 15, 2025

@sinclairzx81 - FYI I've added the 2-hop conversion, so now the API is fully symmetrical (again).

PS. I've been chewing on the idea of a universal type+validation system for a while, and what you've built here is real art.
I'd love to see this become a requirement in the next version of standard-schema - some sort of universal to/from anything using a lightweight core AST (like TypeBox). Given it took them years to pick the property key though, I'm not holding my breath 😂
Edit: Just saw #16 and if you do decide to convene a counsel plz consider tagging me! TBH I thought I was a bit nutty obsessing over these things, glad I'm not alone!
PS. Sorry for all the edits... I tend to get overexcited about types 🤓 😂

@theshadow27
Copy link
Author

Ugh, it seems my excitement was to be short lived. Once I used it in anger (real project), started seeing lots of dropped types. Went back and checked, copilot had straight up dropped the hard ones 😆 😭 so back to doing it old school. I did some hand-coding, and it's now much closer to what it should have been the first time.

@sinclairzx81 Please check the new zod4-tsc-tests.ts, specifically:

  1. ZodCustom - maybe no point
  2. ZodLazy - probably a way to do it through Recursive but I couldn't get it
  3. ZodMap / ZodSet - I don't think there's a direct mapping?
  4. ZodPipe - Maybe similar to promise but I wanted to verify that it makes sense to map something like this
  5. ZodTemplateLiteral - this one feels SO CLOSE but could use a little extra help pushing it over the line
  6. ZodUnion - this is my blocker ATM

Sorry and thanks!

@theshadow27
Copy link
Author

Hi @sinclairnick @sinclairzx81,

Any guidance on how I can get the TemplateLiteral and Union adapters working? Would greatly appreciate TypeBox creator 🧠...

Thanks again for your time!

jd

@marioparaschiv
Copy link

Hey, thank you for working on this! Are there any plans on this PR being pushed forward?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants