chore: upgrade zod to 4.5.4 and drop the manual root $ref hoisting - #1969
Conversation
Zod 4.5.0 shipped colinhacks/zod#6029, which hoists a root schema carrying a registry id into `$defs` and leaves a `$ref` at the root, including RFC 6901 pointer encoding. That is exactly what ZodToJsonSchemaConverter was doing by hand, so the workaround is removed and `@orpc/zod` now requires zod >= 4.5.0. Zod 4.5 also merges object intersections into a single object schema instead of emitting `allOf`, and a root schema with an id is no longer inlined, so two OpenAPI generator e2e expectations are updated to match.
More templates
@orpc/ai-sdk
@orpc/arktype
@orpc/bun
@orpc/client
@orpc/cloudflare
@orpc/contract
@orpc/experimental-effect
@orpc/evlog
@orpc/hibernation
@orpc/json-schema
@orpc/experimental-msw
@orpc/nest
@orpc/next
@orpc/node
@orpc/openapi
@orpc/opentelemetry
@orpc/pinia-colada
@orpc/pino
@orpc/publisher
@orpc/ratelimit
@orpc/server
@orpc/shared
@orpc/swr
@orpc/tanstack-query
@orpc/trpc
@orpc/valibot
@orpc/zod
commit: |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
- zod 4.4.3 → 4.5.4 across the monorepo — every
package.json(root, packages, playgrounds,apps/content) bumpszodto^4.5.4, and@orpc/zod'speerDependenciesfloor moves from>=4.3.5to>=4.5.0. The lockfile keeps the diff zod-only (plus oneexpress-rate-limitpeer-key normalization), and nopnpm-workspace.yamlage-exclude entry is involved. - Native root
$refhoisting replaces the workaround —ZodToJsonSchemaConverterno longer hand-rolls the root-id →{ $ref, $defs }hoist (RFC 6901 pointer encoding included); zod ≥ 4.5.0 does it natively. The fabricated collision test is removed; the two realistic root-$reftests (escaped pointers, custom registry) remain and pass, withconverter.tsstill at 100% coverage. - OpenAPI generator e2e expectations updated — object intersections now emit a single merged object schema instead of
allOf(non-object intersections keepallOf), and an id-carrying root output schema now references#/components/schemas/Planetrather than inlining.
I verified the zod claims empirically: under 4.5.4 a root meta({ id }) schema converts to { $ref: '#/$defs/…', $defs: { … } } with pointer escapes (root~/ → ~0~1), object intersections merge into one object while non-object intersections stay allOf (both checked directly against toJSONSchema), and the generator's param/body extraction already resolves root local refs. The error the removed test fabricated a collision to hide — two schemas sharing an id in one tree — is thrown by zod itself ("Duplicate schema id…") in both 4.4.3 and 4.5.4, so the workaround's name__0 rename branch was only reachable through the mock. pnpm vitest run over the zod, json-schema, openapi, and ai-sdk packages (660 tests) plus the two touched e2e files all pass, as does type:check on the affected packages.
ℹ️ Behavior changes ship under a chore: title
This PR changes generated-OpenAPI output for real users: object intersections no longer emit allOf, and any root schema carrying meta({ id }) now surfaces as a shared component $ref where it previously inlined. Combined with the @orpc/zod peer floor rising to >=4.5.0 — a semver-breaking floor for consumers still pinned to zod 4.4.x — this is more than a mechanical dependency bump. The changelog is drafted from PR titles and the repo doesn't use changesets, so it's worth confirming the next release notes call these out explicitly; chore: may not surface them.
Technical details
# Release-note visibility
## Affected sites
- `packages/openapi/tests/openapi-generator/composed-schemas.test.ts:90` — object-intersection output moved from `allOf` to a merged object; any user generating docs from intersection schemas sees a different document shape.
- `packages/openapi/tests/openapi-generator/crud.test.ts:167` — id-carrying root output now renders as `{ $ref: '#/components/schemas/Planet' }` instead of an inline object.
- `packages/zod/package.json:49` — `peerDependencies.zod` floor raised `>=4.3.5` → `>=4.5.0`.
## Required outcome
- Confirm the next release notes mention (a) the `allOf` → merged-object change for object intersections and (b) the raised zod peer floor.
## Open questions for the human
- Is the `chore:` classification intended given the user-visible OpenAPI changes and the peer-floor bump?
- Should the zod/openapi docs pages note the intersection-merging behavior? (Nothing in `apps/content/docs` currently references `allOf`, so no content is stale.)DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

Upgrades zod from 4.4.3 to 4.5.4 across the monorepo and removes the manual root
$refhoisting fromZodToJsonSchemaConverter, which zod now does natively.@orpc/zodrequires zod>=4.5.0as a result.Fixes
colinhacks/zod#6029, the PR the workaround comment pointed at, shipped in zod 4.5.0. A root schema carrying a registry id is now hoisted into
$defswith a$refleft at the root, RFC 6901 pointer encoding included, so the hand-rolled version in the converter is redundant. The version floor was confirmed by converting a schema with.meta({ id: "User" })under each release: 4.4.3 inlines it, 4.5.0 emits{ $ref: "#/$defs/User", $defs: { User: {...} } }.Behavior changes from zod 4.5
Two OpenAPI generator e2e expectations changed. Both come from zod itself, not from removing the workaround — they reproduce with the old converter still in place.
allOf. Non-object intersections (two constrained strings, say) still emitallOf. This is visible in generated OpenAPI documents for anyone using intersection schemas and may be worth a release note.findresponse in the crud test is now a$refto the sharedPlanetcomponent instead of an inlined copy. That brings the assertion in line with the test's own name.The generator's own
allOfpath, which comes from combining multiple.input()/.output()calls rather than from zod, stays covered byopenapi-generator.test.tsand a sibling test in the same file.Testing
pnpm testpasses (3310 tests, plus the bun, cloudflare, and nest suites), as dopnpm type:checkandpnpm lint.packages/zod/src/converter.tsremains at 100% statement, branch, function, and line coverage, so the removal left nothing unreachable behind.The dropped converter test fabricated a colliding
$defsentry through a mockedtoJSONSchemato exercise the workaround'sroot__0rename. That collision cannot occur now that zod writes the root def into the same map it builds. The two remaining root-$reftests still cover escaped JSON pointers and a custom metadata registry.The lockfile diff is zod-only aside from pnpm peer-key normalization, which changed no package versions.