You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Nothing owns the shape of a generated Zod export block — the export const, its companion
types, and its Item companion. That shape lives in eleven template literals spread over two
packages, and the two sets have drifted apart. Per-operation schemas get branding and an Item
split but never companion types; reusable component schemas get companion types and a recursive
pin but never branding or an Item split. Both behaviours are fine on their own. What isn't
fine is that neither is written down anywhere except as repeated string concatenation, so
changing export shape means editing eleven places and reconciling two conventions by hand.
You can see the split in the output: tests/__snapshots__/zod/petstore/endpoints.ts has 52 export const and 0 export type, while component-schema output ships both type aliases for
every schema. Hono's *.zod.ts files inherit the per-operation form, since they route through
the same builder.
It also makes the tests awkward. packages/zod/src/zod.test.ts is 12,335 lines with 79
assertions against generateZod(...).implementation, 45 of which fabricate input with } as unknown as Parameters<typeof generateZod>[0]. Those tests want to check what one export
block looks like, but the smallest thing to assert against is the whole client builder.
Callers of generateZod outside @orval/zod: packages/hono/src/index.ts:35, 909, 962.
Proposed change
Add packages/zod/src/export-emitter.ts, sibling to compatible-v4.ts, and route all eleven
sites through it. No new dependency edges: orval and @orval/hono already depend on @orval/zod. @orval/effect is out of scope — it only depends on @orval/core.
Make it a pure renderer: descriptor in, string out. No ContextSpec, no OpenAPI, so it can be
tested with plain objects. Sketch, not a settled API:
It owns: companion types and the input/Output pairing; brand placement (on the wrapper,
never on Item); the Item split, including the Mini-vs-classic bounds syntax now in zodArrayWithBounds; the recursive pin.
It does not own: name allocation (allocateExportName needs ref context and stays put); the
recursive TS body (renderReusableSchemaEntry keeps resolveValue, sub-models, and extra
imports, and passes tsBody in — pulling that in would need fixtures and lose the point of the
module); hoisted consts (…Default, …RegExp0), which keep crossing as pre-rendered strings.
Why @orval/zod and not @orval/core
#3650 adds a similar emitter and puts it in core. Suggested rule that fits both: a shared
emitter lives in the lowest package all its callers already depend on. For #3650 that's core
(angular, fetch, and query don't depend on @orval/zod); for this one it's @orval/zod (zod,
orval, and hono all do). Putting it in core would push Zod-specific syntax — brand form, zod.input/zod.output, the Mini functional API — below the package that owns it, with no gain
in reach. Easy to move if maintainers prefer one location.
Acceptance
pnpm test:snapshots shows zero diff across every tracked snapshot file. This change is
behaviour-preserving, and that is the whole review.
All eleven export templates in packages/zod/src/index.ts and packages/orval/src/write-zod-specs.ts render through renderZodExport; no export const
string concatenation is left in either file for these blocks.
New packages/zod/src/export-emitter.test.ts with plain-object unit tests, modelled on compatible-v4.test.ts, covering: companion types on/off, brand on/off, Item split in both
classic and Mini with and without min/max bounds, and the recursive pin.
Out of scope: moving the 79 existing result.implementation assertions onto the new interface.
It wouldn't change the zero-diff proof and would roughly triple the diff on the PR that most
needs careful reading. Better as a follow-up, tests only.
feat(core): add formatType config for custom string format types #3727 (open PR) — the only open PR touching packages/zod/src/index.ts. Its hunks sit in generateZodValidationSchemaDefinition and the top of generateZod, above the export
templates. Same-file rebase only. Nothing open touches write-zod-specs.ts or compatible-v4.ts.
What's wrong
Nothing owns the shape of a generated Zod export block — the
export const, its companiontypes, and its
Itemcompanion. That shape lives in eleven template literals spread over twopackages, and the two sets have drifted apart. Per-operation schemas get branding and an
Itemsplit but never companion types; reusable component schemas get companion types and a recursive
pin but never branding or an
Itemsplit. Both behaviours are fine on their own. What isn'tfine is that neither is written down anywhere except as repeated string concatenation, so
changing export shape means editing eleven places and reconciling two conventions by hand.
You can see the split in the output:
tests/__snapshots__/zod/petstore/endpoints.tshas 52export constand 0export type, while component-schema output ships both type aliases forevery schema. Hono's
*.zod.tsfiles inherit the per-operation form, since they route throughthe same builder.
It also makes the tests awkward.
packages/zod/src/zod.test.tsis 12,335 lines with 79assertions against
generateZod(...).implementation, 45 of which fabricate input with} as unknown as Parameters<typeof generateZod>[0]. Those tests want to check what one exportblock looks like, but the smallest thing to assert against is the whole client builder.
Where
Verified against master (
70fc4fad4).packages/zod/src/index.ts— 8 literals, 10export conststatements:<Op>Params— 3370<Op>QueryParams— 3376<Op>Header— 3382<Op>Body±Item— 3389-3391<Op>Response, no content — 3431<Op>Response±Item— 3438-3440Helpers they lean on:
brand(3288),zodArrayWithBounds(3295),allocateExportName(3333).packages/orval/src/write-zod-specs.ts— 3 literals:renderReusableSchemaEntry, 295)Callers of
generateZodoutside@orval/zod:packages/hono/src/index.ts:35, 909, 962.Proposed change
Add
packages/zod/src/export-emitter.ts, sibling tocompatible-v4.ts, and route all elevensites through it. No new dependency edges:
orvaland@orval/honoalready depend on@orval/zod.@orval/effectis out of scope — it only depends on@orval/core.Make it a pure renderer: descriptor in, string out. No
ContextSpec, no OpenAPI, so it can betested with plain objects. Sketch, not a settled API:
It owns: companion types and the
input/Outputpairing; brand placement (on the wrapper,never on
Item); theItemsplit, including the Mini-vs-classic bounds syntax now inzodArrayWithBounds; the recursive pin.It does not own: name allocation (
allocateExportNameneeds ref context and stays put); therecursive TS body (
renderReusableSchemaEntrykeepsresolveValue, sub-models, and extraimports, and passes
tsBodyin — pulling that in would need fixtures and lose the point of themodule); hoisted consts (
…Default,…RegExp0), which keep crossing as pre-rendered strings.Why
@orval/zodand not@orval/core#3650 adds a similar emitter and puts it in core. Suggested rule that fits both: a shared
emitter lives in the lowest package all its callers already depend on. For #3650 that's core
(angular, fetch, and query don't depend on
@orval/zod); for this one it's@orval/zod(zod,orval, and hono all do). Putting it in core would push Zod-specific syntax — brand form,
zod.input/zod.output, the Mini functional API — below the package that owns it, with no gainin reach. Easy to move if maintainers prefer one location.
Acceptance
pnpm test:snapshotsshows zero diff across every tracked snapshot file. This change isbehaviour-preserving, and that is the whole review.
packages/zod/src/index.tsandpackages/orval/src/write-zod-specs.tsrender throughrenderZodExport; noexport conststring concatenation is left in either file for these blocks.
packages/zod/src/export-emitter.test.tswith plain-object unit tests, modelled oncompatible-v4.test.ts, covering: companion types on/off, brand on/off,Itemsplit in bothclassic and Mini with and without min/max bounds, and the recursive pin.
Out of scope: moving the 79 existing
result.implementationassertions onto the new interface.It wouldn't change the zero-diff proof and would roughly triple the diff on the PR that most
needs careful reading. Better as a follow-up, tests only.
Related
bothstrategy for Zod-backed clients #3650 (open PR) — same conclusion from the same starting point. See the location note abovefor why the two modules land in different packages.
write-zod-specs.tstemplates this consolidates.packages/zod/src/index.ts. Its hunks sit ingenerateZodValidationSchemaDefinitionand the top ofgenerateZod, above the exporttemplates. Same-file rebase only. Nothing open touches
write-zod-specs.tsorcompatible-v4.ts.