feat(zod): add generateReusableSchemas opt-in flag - #3449
Conversation
Adds `override.zod.generateReusableSchemas` to emit one reusable Zod schema per `#/components/schemas/*` $ref instead of inlining. Operations and other schemas reference the named export (with `namingConvention` applied), and cycles are wrapped in `z.lazy(() => Name)` only where they close a cycle. Highlights: - `generateZodValidationSchemaDefinition` recognizes `$ref` input, emits a `namedRef` placeholder + chained modifiers for nullable, default, description, and parent-driven optional. Non-chainable siblings fall back to inlining at that one site. - `parseZodValidationSchemaDefinition` returns the set of refs each schema uses and renders them as `__REF_<name>__` sentinels. - New `packages/orval/src/reusable-schemas.ts` orchestrator: collects reachable component refs, runs the generator per root, computes Tarjan SCC + DFS back-edges, rewrites sentinels to direct refs or `z.lazy(...)` and emits in topological order. - `writeZodSchemas` routes through the orchestrator when the flag is on, producing per-file output with cross-file imports. - `writeZodSchemasFromVerbs` skips per-operation wrapper files when the body/response is a pure $ref. - `generateZod` (verb-level) threads the flag, rewrites sentinels, and emits the used refs as imports so operation files cross-import component schemas. - `write-specs.ts` auto-promotes `schemas: <string>` to the zod writer when `client: 'zod'` + flag is on, so the schemas dir contains zod schemas instead of TS types. - Sample: existing `swr-with-zod` adds a `petstoreZodReusable` config exercising the flag end-to-end (snapshots included). Default is off; existing output is unchanged when the flag is unset.
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds a config flag to enable reusable Zod schema generation and implements end-to-end support: name resolution, reachable-ref collection, per-ref Zod entry generation with usedRefs, dependency/SCC analysis to select lazy edges, sentinel rewriting into imports or zod.lazy, Orval writer integration, tests, docs, and sample snapshots. ChangesReusable Zod Schemas Pipeline
Estimated code review effort: Possibly related issues:
Possibly related PRs:
Suggested reviewers:
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adds a generateReusableSchemas mode for the Zod generator to emit reusable, cross-file component schemas (with cycle-safe references) instead of inlining $refd schemas everywhere.
Changes:
- Introduces
override.zod.generateReusableSchemasoption and wires it through normalization/config/types. - Implements
$ref-aware “namedRef” placeholders in@orval/zod, plus ref tracking and import emission. - Adds a reusable-schema orchestrator in
@orval/orval(graph + SCC +zod.lazy) and updates sample outputs/snapshots/tests.
Reviewed changes
Copilot reviewed 36 out of 36 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| samples/swr-with-zod/src/gen/models/pets-reusable.zod.ts | Adds generated reusable Zod schema for pets. |
| samples/swr-with-zod/src/gen/models/pet-reusable.zod.ts | Adds generated reusable Zod schema for pet with refs to cat/dog. |
| samples/swr-with-zod/src/gen/models/listPetsParams-reusable.zod.ts | Adds generated reusable Zod schema for ListPetsParams. |
| samples/swr-with-zod/src/gen/models/labradoodle-reusable.zod.ts | Adds generated reusable Zod schema for labradoodle. |
| samples/swr-with-zod/src/gen/models/index.ts | Re-exports new *-reusable.zod schema modules. |
| samples/swr-with-zod/src/gen/models/error-reusable.zod.ts | Adds generated reusable Zod schema for error. |
| samples/swr-with-zod/src/gen/models/dog-reusable.zod.ts | Adds generated reusable Zod schema for dog with refs. |
| samples/swr-with-zod/src/gen/models/dachshund-reusable.zod.ts | Adds generated reusable Zod schema for dachshund. |
| samples/swr-with-zod/src/gen/models/createPetsBody-reusable.zod.ts | Adds generated reusable Zod schema for request body. |
| samples/swr-with-zod/src/gen/models/cat-reusable.zod.ts | Adds generated reusable Zod schema for cat. |
| samples/swr-with-zod/src/gen/endpoints/pets/pets-reusable.zod.ts | Adds generated endpoint validators that import reusable schemas. |
| samples/swr-with-zod/orval.config.ts | Adds a sample config target enabling reusable Zod schemas. |
| samples/swr-with-zod/snapshots/models/pets-reusable.zod.ts | Snapshot for generated reusable pets schema. |
| samples/swr-with-zod/snapshots/models/pet-reusable.zod.ts | Snapshot for generated reusable pet schema. |
| samples/swr-with-zod/snapshots/models/listPetsParams-reusable.zod.ts | Snapshot for generated reusable ListPetsParams schema. |
| samples/swr-with-zod/snapshots/models/labradoodle-reusable.zod.ts | Snapshot for generated reusable labradoodle schema. |
| samples/swr-with-zod/snapshots/models/index.ts | Snapshot updates to include new exports. |
| samples/swr-with-zod/snapshots/models/error-reusable.zod.ts | Snapshot for generated reusable error schema. |
| samples/swr-with-zod/snapshots/models/dog-reusable.zod.ts | Snapshot for generated reusable dog schema. |
| samples/swr-with-zod/snapshots/models/dachshund-reusable.zod.ts | Snapshot for generated reusable dachshund schema. |
| samples/swr-with-zod/snapshots/models/createPetsBody-reusable.zod.ts | Snapshot for generated reusable CreatePetsBody schema. |
| samples/swr-with-zod/snapshots/models/cat-reusable.zod.ts | Snapshot for generated reusable cat schema. |
| samples/swr-with-zod/snapshots/endpoints/pets/pets-reusable.zod.ts | Snapshot for generated endpoint validators. |
| packages/zod/src/zod.test.ts | Adds unit tests for namedRef emission + parsing/tracking. |
| packages/zod/src/index.ts | Implements $ref placeholder emission, sentinel rewriting, and import generation. |
| packages/solid-start/src/index.test.ts | Updates test context default zod options to include the new flag. |
| packages/orval/src/write-zod-specs.ts | Adds reusable-schema writer and inline generation path, plus plumbing. |
| packages/orval/src/write-zod-specs.test.ts | Adds tests ensuring cross-file imports + wrapper skipping behavior. |
| packages/orval/src/write-specs.ts | Routes schema writing through Zod writer when reusable mode is enabled. |
| packages/orval/src/utils/options.ts | Normalizes generateReusableSchemas into output override defaults. |
| packages/orval/src/utils/options.test.ts | Adds test that the new flag defaults to false. |
| packages/orval/src/reusable-schemas.ts | New reusable-schema orchestrator: graph building, SCC/lazy-edge computing, rewriting. |
| packages/orval/src/reusable-schemas.test.ts | Adds coverage for name resolution, reachable refs, SCC/lazy rewriting. |
| packages/mock/src/faker/getters/combine.test.ts | Updates mock test context defaults to include the new flag. |
| packages/core/src/types.ts | Extends ZodOptions/NormalizedZodOptions with generateReusableSchemas. |
| packages/core/src/test-utils/context.ts | Updates test context defaults to include the new flag. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/zod/src/index.ts (1)
553-563:⚠️ Potential issue | 🟠 Major | ⚡ Quick winFix the tuple-item recursion inputs.
Line 554 still dereferences each
prefixItemsentry, so reusable mode can never emit anamedReffor tuple members, and Lines 557-558 passisZodV4/strictin the opposite order. Tuple items will be inlined unexpectedly or generated with the wrong Zod/strict mode.Suggested fix
prefixItems.map((item, idx) => generateZodValidationSchemaDefinition( - dereference(item, context), + useReusableSchemas ? item : dereference(item, context), context, camel(`${name}-${idx}-item`), - isZodV4, strict, + isZodV4, { required: true, constNameRegistry, useReusableSchemas, },🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/zod/src/index.ts` around lines 553 - 563, The tuple item call to generateZodValidationSchemaDefinition is wrong: stop dereferencing each prefixItems entry so reusable schemas can emit namedRef (pass the raw item instead of dereference(item, context)), and swap the isZodV4/strict boolean arguments so their order matches the function signature (pass strict then isZodV4); keep context, the camel name, and the options object (required/constNameRegistry/useReusableSchemas) unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/orval/src/reusable-schemas.ts`:
- Around line 78-85: The current logic in collectRefsInValue adds record.$ref to
refs and returns early, which skips sibling properties; change it so that when
isComponentSchemaRef(record.$ref) is true you still add record.$ref to refs but
do NOT return from the function — instead skip recursing into the $ref property
itself and continue iterating the other keys on record so siblings are still
processed. Update collectRefsInValue to only avoid descending into the $ref
value (record.$ref) while continuing to call collectRefsInValue on the other
properties; keep using isComponentSchemaRef, refs and record.$ref to locate and
handle the case.
- Around line 274-299: computeLazyEdges currently marks lazy edges using the DFS
order (dfs, stackSet, lazy, edgeKey) but topoSortEntries later reorders nodes
inside each SCC lexicographically, which can make a forward edge non-lazy while
the source is emitted before the target and trigger TDZ; fix by making
edge-laziness and emit order consistent: either derive the emit order from the
same DFS that sets lazy edges (use the dfs visitation order / recorded
start/visited sequence for topoSortEntries) or, when topo-sorting an SCC (scc),
compute lazy edges after you sort the SCC so lazy reflects the final emit order
(ensure computeLazyEdges and topoSortEntries agree on node order and update lazy
markings accordingly).
In `@packages/orval/src/write-specs.ts`:
- Around line 283-286: The current isZodSchemas check incorrectly allows
auto-promotion to Zod when output.client === 'zod' even if output.schemas is an
explicit object; change the condition so the client-based promotion only applies
when output.schemas is a string (i.e., a named preset) — specifically update the
isZodSchemas logic to require isString(output.schemas) for the branch that
checks output.client === 'zod' and output.override.zod.generateReusableSchemas,
leaving the original path that detects non-string output.schemas.type === 'zod'
intact; locate the isZodSchemas declaration and adjust the boolean expression
accordingly.
In `@packages/orval/src/write-zod-specs.ts`:
- Around line 569-571: The code is passing unresolved component $ref schemas
downstream when useReusableSchemas is true because you only dereference when
false; update the wrapper rendering path in writeZodSchemasFromVerbs (and the
branches handling schema: bodySchema/querySchema/headerSchema/responseSchema) so
that when useReusableSchemas is true you do NOT dereference and the wrapper
writer is invoked with the reusable rendering path (i.e., ensure the wrapper
generation uses the reusable export/import logic instead of the inline generator
path); conversely, only call dereference(bodySchema, zodContext) (and the
equivalent dereference calls at the other branches) when useReusableSchemas is
false so nested component refs resolve consistently to shared exports.
In `@packages/zod/src/index.ts`:
- Around line 1527-1534: The branch that sets effectiveSchema allows a pure $ref
request body to bypass removal of readOnly fields; change the logic so when
useReusableSchemas is true and parseType === 'body' you detect if schema is an
OpenApiReferenceObject (i.e., has a $ref) and in that case use
removeReadOnlyProperties(resolvedJsonSchema) instead of forwarding schema;
update the conditional that assigns effectiveSchema (refer to effectiveSchema,
useReusableSchemas, parseType, schema, resolvedJsonSchema, and
removeReadOnlyProperties) to prefer the processed resolvedJsonSchema for $ref
body schemas while still reusing raw schema for non-body or non-$ref cases.
- Around line 1654-1668: The current logic eagerly resolves parameter.schema via
resolveRef causing component $ref (e.g. "`#/components/schemas/`*") to be inlined
instead of preserved for the namedRef path; change the schemaForGen branch so
that when useReusableSchemas is true you first detect if parameter.schema is a
reference (i.e. has a $ref) and, if so, return that reference directly (or a
shallow copy that adds parameter.description without mutating the original)
rather than calling resolveRef; only call resolveRef for non-$ref schemas so
namedRef handling can still see and export reusable component refs (affecting
symbols: schemaForGen, useReusableSchemas, parameter.schema, resolveRef,
namedRef).
---
Outside diff comments:
In `@packages/zod/src/index.ts`:
- Around line 553-563: The tuple item call to
generateZodValidationSchemaDefinition is wrong: stop dereferencing each
prefixItems entry so reusable schemas can emit namedRef (pass the raw item
instead of dereference(item, context)), and swap the isZodV4/strict boolean
arguments so their order matches the function signature (pass strict then
isZodV4); keep context, the camel name, and the options object
(required/constNameRegistry/useReusableSchemas) unchanged.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 50f9c99f-1838-4897-8b5b-e6b620e63e43
⛔ Files ignored due to path filters (11)
samples/swr-with-zod/src/gen/endpoints/pets/pets-reusable.zod.tsis excluded by!**/gen/**samples/swr-with-zod/src/gen/models/cat-reusable.zod.tsis excluded by!**/gen/**samples/swr-with-zod/src/gen/models/createPetsBody-reusable.zod.tsis excluded by!**/gen/**samples/swr-with-zod/src/gen/models/dachshund-reusable.zod.tsis excluded by!**/gen/**samples/swr-with-zod/src/gen/models/dog-reusable.zod.tsis excluded by!**/gen/**samples/swr-with-zod/src/gen/models/error-reusable.zod.tsis excluded by!**/gen/**samples/swr-with-zod/src/gen/models/index.tsis excluded by!**/gen/**samples/swr-with-zod/src/gen/models/labradoodle-reusable.zod.tsis excluded by!**/gen/**samples/swr-with-zod/src/gen/models/listPetsParams-reusable.zod.tsis excluded by!**/gen/**samples/swr-with-zod/src/gen/models/pet-reusable.zod.tsis excluded by!**/gen/**samples/swr-with-zod/src/gen/models/pets-reusable.zod.tsis excluded by!**/gen/**
📒 Files selected for processing (25)
packages/core/src/test-utils/context.tspackages/core/src/types.tspackages/mock/src/faker/getters/combine.test.tspackages/orval/src/reusable-schemas.test.tspackages/orval/src/reusable-schemas.tspackages/orval/src/utils/options.test.tspackages/orval/src/utils/options.tspackages/orval/src/write-specs.tspackages/orval/src/write-zod-specs.test.tspackages/orval/src/write-zod-specs.tspackages/solid-start/src/index.test.tspackages/zod/src/index.tspackages/zod/src/zod.test.tssamples/swr-with-zod/__snapshots__/endpoints/pets/pets-reusable.zod.tssamples/swr-with-zod/__snapshots__/models/cat-reusable.zod.tssamples/swr-with-zod/__snapshots__/models/createPetsBody-reusable.zod.tssamples/swr-with-zod/__snapshots__/models/dachshund-reusable.zod.tssamples/swr-with-zod/__snapshots__/models/dog-reusable.zod.tssamples/swr-with-zod/__snapshots__/models/error-reusable.zod.tssamples/swr-with-zod/__snapshots__/models/index.tssamples/swr-with-zod/__snapshots__/models/labradoodle-reusable.zod.tssamples/swr-with-zod/__snapshots__/models/listPetsParams-reusable.zod.tssamples/swr-with-zod/__snapshots__/models/pet-reusable.zod.tssamples/swr-with-zod/__snapshots__/models/pets-reusable.zod.tssamples/swr-with-zod/orval.config.ts
- Preserve $refs verbatim in `parseParameters` when `useReusableSchemas` is on (previously called `resolveRef`, stripping the wrapper and bypassing the namedRef path for parameter schemas). - Thread `useReusableSchemas` into the inner `generateZodValidationSchemaDefinition` / `generateFormDataZodSchema` calls in `writeZodSchemasFromVerbs` so nested refs inside non-pure-ref body/response wrappers emit namedRef placeholders. - Restore `.zod.ts` default file extension in `write-specs.ts`: the normalizer always sets `output.fileExtension` (default `.ts`), so the previous `|| '.zod.ts'` fallback was dead. Treat the bare default as unset; honour any user-set extension. - Reject `namingConvention: 'kebab-case'` (and any other convention that produces a non-identifier name) for reusable schema export names — would otherwise emit `export const pet-owner = ...` which is invalid TS. - Sort `usedRefs` before mapping to imports in `generateZod` so import lines are stable across runs regardless of Set iteration order. - Document `generateReusableSchemas` in `docs/content/docs/reference/configuration/output.mdx` (per @melloware's review).
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
packages/orval/src/reusable-schemas.ts (2)
271-315:⚠️ Potential issue | 🟠 Major | 🏗️ Heavy liftOne lazy edge per SCC is unsafe once schemas are emitted as separate files.
This back-edge strategy works for single-file output, but
writeZodSchemasReusablewrites one module per schema and imports everyusedRef. In a cycle likeA <-> B, leavingA -> Bdirect and only lazifyingB -> Astill produces a TDZ path when the consumer importsBfirst:BloadsA,AreadsBbeforeBfinishes initializing. For split output, every intra-SCC reference needs to be lazy, or the whole SCC needs to be emitted together.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/orval/src/reusable-schemas.ts` around lines 271 - 315, computeLazyEdges currently only adds a single back-edge per SCC which is unsafe for split-file output; change it so that for any non-trivial SCC (scc from tarjan) you mark every intra-SCC directed edge as lazy (i.e., call edgeKey(u,v) and add to the lazy set for every u in scc and every v in graph.get(u) that is contained in sccSet), preserving the existing self-loop handling; update references to computeLazyEdges/edgeKey/tarjan so writeZodSchemasReusable will treat all intra-SCC references as lazy imports rather than relying on a single back-edge.
27-53:⚠️ Potential issue | 🔴 Critical | ⚡ Quick winReject reserved JS keywords for generated reusable schema exports
JS_IDENTIFIER_PATTERNonly checks identifier shape (not whether the result is a reserved keyword), so names likedefault(e.g.#/components/schemas/Default→defaultundercamelCase) can pass validation and then be emitted asexport const default = ..., which is invalid syntax. Add a reserved-word/keyword blacklist (or use a JS/TS identifier validator that rejects keywords) inresolveSchemaNames.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/orval/src/reusable-schemas.ts` around lines 27 - 53, The current JS_IDENTIFIER_PATTERN only checks identifier shape and allows reserved JS/TS keywords (e.g. "default") which would produce invalid exports; update resolveSchemaNames to also reject reserved words by adding a blacklist of JS/TS reserved keywords (or integrate a validator that checks keywords) and throw an Error when resolveSchemaName(ref, namingConvention) yields a reserved word; reference JS_IDENTIFIER_PATTERN, resolveSchemaNames, and resolveSchemaName when locating where to add the keyword check and the error message.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/orval/src/write-specs.ts`:
- Around line 289-293: The code infers an "unset" extension by checking
output.fileExtension === '.ts', which loses the distinction between a normalized
default and an explicit user value; update the normalizer to carry an explicit
flag (e.g. output.fileExtensionExplicit or similar) alongside
output.fileExtension, update the normalization code that produces output to set
that flag when the user provided an extension, and change the logic in
write-specs.ts (where fileExtension is computed) to use that explicit flag: use
'.zod.ts' only when fileExtensionExplicit is false/undefined, otherwise honor
output.fileExtension exactly; ensure any callers that construct/normalize output
objects propagate the new flag.
---
Outside diff comments:
In `@packages/orval/src/reusable-schemas.ts`:
- Around line 271-315: computeLazyEdges currently only adds a single back-edge
per SCC which is unsafe for split-file output; change it so that for any
non-trivial SCC (scc from tarjan) you mark every intra-SCC directed edge as lazy
(i.e., call edgeKey(u,v) and add to the lazy set for every u in scc and every v
in graph.get(u) that is contained in sccSet), preserving the existing self-loop
handling; update references to computeLazyEdges/edgeKey/tarjan so
writeZodSchemasReusable will treat all intra-SCC references as lazy imports
rather than relying on a single back-edge.
- Around line 27-53: The current JS_IDENTIFIER_PATTERN only checks identifier
shape and allows reserved JS/TS keywords (e.g. "default") which would produce
invalid exports; update resolveSchemaNames to also reject reserved words by
adding a blacklist of JS/TS reserved keywords (or integrate a validator that
checks keywords) and throw an Error when resolveSchemaName(ref,
namingConvention) yields a reserved word; reference JS_IDENTIFIER_PATTERN,
resolveSchemaNames, and resolveSchemaName when locating where to add the keyword
check and the error message.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: f7d58470-3424-46d5-9ae3-1bbb07962861
📒 Files selected for processing (6)
docs/content/docs/reference/configuration/output.mdxpackages/orval/src/reusable-schemas.test.tspackages/orval/src/reusable-schemas.tspackages/orval/src/write-specs.tspackages/orval/src/write-zod-specs.tspackages/zod/src/index.ts
|
i believe this fixes #2535 |
- Stop early-returning at `$ref` in `collectRefsInValue`: OpenAPI 3.1
allows siblings with their own refs; only skip descending into the
`$ref` string itself.
- Integrate back-edge detection into Tarjan and use the SCC's natural
pop order for emit ordering. The previous code lex-sorted nodes
within an SCC, which could leave a non-lazy forward edge pointing
to a not-yet-emitted node and trigger a TDZ at module load.
- Restrict the auto-promote-to-zod branch in `write-specs.ts` to
string `schemas:` configs only. Explicit `{ type: 'typescript' }`
configs are kept on the TS path even when `client: 'zod'` + the
flag are set.
- Document the `readOnly` trade-off: with the flag on, shared
component schemas keep `readOnly` properties in request body
validators (previously stripped via `dereference + removeReadOnlyProperties`).
The previous fix in write-specs.ts inferred "unset" by checking
output.fileExtension === '.ts', which couldn't distinguish the
normalizer default from a user-set '.ts'. Move the default into
normalization where we still see the raw outputOptions: when the
output is zod-schemas (explicit `{ type: 'zod' }` OR a string
`schemas:` + `client: 'zod'` + `generateReusableSchemas`), default
fileExtension to `.zod.ts`; otherwise '.ts'. The write-specs.ts
branch now reads output.fileExtension directly with no override.
A user-set fileExtension still wins via the `?? defaultFileExtension`
chain.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/orval/src/utils/options.ts`:
- Around line 221-232: The change flips the shared defaultFileExtension causing
non-schema writers to get the .zod.ts suffix; instead, keep defaultFileExtension
as the general output suffix and introduce a schema-specific extension
flag/variable used only by schema writers: retain defaultFileExtension as '.ts',
create a new schemaFileExtension (or a boolean signal like isZodSchemasOutput)
scoped to schema emitters (reference isZodSchemasOutput and
writeFakerSchemaMocks) and use that when generating schema filenames, ensuring
other writers still read output.fileExtension unchanged.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 608754e1-006b-4e1e-aaf4-f2c8bfd6b654
📒 Files selected for processing (4)
docs/content/docs/reference/configuration/output.mdxpackages/orval/src/reusable-schemas.tspackages/orval/src/utils/options.tspackages/orval/src/write-specs.ts
✅ Files skipped from review due to trivial changes (1)
- docs/content/docs/reference/configuration/output.mdx
yep |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 38 out of 38 changed files in this pull request and generated 4 comments.
Comments suppressed due to low confidence (1)
samples/swr-with-zod/src/gen/models/index.ts:1
- Re-exporting both the original model modules and the new
*-reusable.zodmodules from the same barrel can create ambiguous/unstable public exports (e.g., consumers importingpetmay now get a runtime Zod schema value instead of only types) and can also trigger duplicate export-name conflicts if both sides export the same type names. Consider separating reusable Zod exports into a dedicated barrel (e.g.models/zod/index.ts) or aliasing the Zod exports (e.g.petSchema,petInput,petOutput) to avoid collisions and accidental runtime imports.
- Scope the `.zod.ts` default to schema writers only: introduce `output.schemaFileExtension` so non-schema writers (client / mock / workspace barrel) keep their own extensions and don't leak the zod suffix when no `fileExtension` is set. `output.fileExtension` keeps its `.ts` default. User-set `fileExtension` still flows to both. - Unify ref naming between the generator and the orchestrator: the orchestrator's `lastRefSegment` now URL-decodes and unescapes JSON Pointer tokens (`~1`/`~0`), matching `getRefInfo(...).originalName` used by the namedRef emission. Avoids drift on refs with escaped characters. - Use `constNameRegistry` for ref-default const names. Without it, multiple defaults sharing the same `name` would collide on `<name>Default`. Matches the suffix pattern used elsewhere in `generateZodValidationSchemaDefinition`. - Move the stray `rewriteReusableSchemas` import in `reusable-schemas.test.ts` up to the main import block. - Switch `collectReachableComponentRefs` BFS from `queue.shift()` to index-based iteration — O(n) instead of O(n²) for large specs. Also update the various test-utils fixtures that build `NormalizedOutputOptions` literals to include the new `schemaFileExtension` field.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/orval/src/utils/options.ts`:
- Around line 312-313: The normalization currently sets schemaFileExtension to
outputOptions.fileExtension and ignores an explicit schema-specific override;
update the assignment so schemaFileExtension uses the schema-specific value
first (e.g., outputOptions.schemaFileExtension ?? outputOptions.fileExtension ??
defaultSchemaFileExtension) so an explicit schema override is honored; locate
the schemaFileExtension assignment in the normalization logic and replace the
current expression accordingly.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: eb4ffeba-e541-4806-9995-02b595f91a94
📒 Files selected for processing (12)
packages/angular/src/http-client.test.tspackages/angular/src/http-resource.test.tspackages/core/src/test-utils/context.tspackages/core/src/test-utils/split-modes.tspackages/core/src/types.tspackages/mock/src/faker/getters/combine.test.tspackages/orval/src/reusable-schemas.test.tspackages/orval/src/reusable-schemas.tspackages/orval/src/utils/options.tspackages/orval/src/write-specs.tspackages/solid-start/src/index.test.tspackages/zod/src/index.ts
melloware
left a comment
There was a problem hiding this comment.
looks like a lint issue
- Honor an explicit `output.schemaFileExtension` user-set value during normalization. Added the field to `OutputOptions` and prioritised it over `outputOptions.fileExtension` and the default. This lets users set a separate extension for schema files (e.g. `.ts` for client + `.zod.ts` for schemas) without affecting the global one. - Fix `.default().optional()` ordering in the reusable-ref path. In zod, `.optional()` after `.default()` short-circuits on undefined and the default never runs. Reordered the ref-branch to match the main path (nullable/nullish/optional → default → describe) and skip `.optional()` when a default is set. - Expand the orchestrator's root set to the transitive closure of reachable component-schema refs. Without this, a generated schema could emit a sentinel for a ref not in the seed list, producing an unresolved identifier in the rewritten output. Tests added: - skip .optional() when default is set - emit .nullish() when nullable + !required (combines nullable+optional) - transitive-closure expansion in generateReusableSchemaSet
Adds `override.zod.generateReusableSchemas` to emit one reusable Zod schema per `#/components/schemas/*` $ref instead of inlining. Operations and other schemas reference the named export (with `namingConvention` applied), and cycles are wrapped in `z.lazy(() => Name)` only where they close a cycle. Highlights: - `generateZodValidationSchemaDefinition` recognizes `$ref` input, emits a `namedRef` placeholder + chained modifiers for nullable, default, description, and parent-driven optional. Non-chainable siblings fall back to inlining at that one site. - `parseZodValidationSchemaDefinition` returns the set of refs each schema uses and renders them as `__REF_<name>__` sentinels. - New `packages/orval/src/reusable-schemas.ts` orchestrator: collects reachable component refs, runs the generator per root, computes Tarjan SCC + DFS back-edges, rewrites sentinels to direct refs or `z.lazy(...)` and emits in topological order. - `writeZodSchemas` routes through the orchestrator when the flag is on, producing per-file output with cross-file imports. - `writeZodSchemasFromVerbs` skips per-operation wrapper files when the body/response is a pure $ref. - `generateZod` (verb-level) threads the flag, rewrites sentinels, and emits the used refs as imports so operation files cross-import component schemas. - `write-specs.ts` auto-promotes `schemas: <string>` to the zod writer when `client: 'zod'` + flag is on, so the schemas dir contains zod schemas instead of TS types. - Sample: existing `swr-with-zod` adds a `petstoreZodReusable` config exercising the flag end-to-end (snapshots included). Default is off; existing output is unchanged when the flag is unset.
- Preserve $refs verbatim in `parseParameters` when `useReusableSchemas` is on (previously called `resolveRef`, stripping the wrapper and bypassing the namedRef path for parameter schemas). - Thread `useReusableSchemas` into the inner `generateZodValidationSchemaDefinition` / `generateFormDataZodSchema` calls in `writeZodSchemasFromVerbs` so nested refs inside non-pure-ref body/response wrappers emit namedRef placeholders. - Restore `.zod.ts` default file extension in `write-specs.ts`: the normalizer always sets `output.fileExtension` (default `.ts`), so the previous `|| '.zod.ts'` fallback was dead. Treat the bare default as unset; honour any user-set extension. - Reject `namingConvention: 'kebab-case'` (and any other convention that produces a non-identifier name) for reusable schema export names — would otherwise emit `export const pet-owner = ...` which is invalid TS. - Sort `usedRefs` before mapping to imports in `generateZod` so import lines are stable across runs regardless of Set iteration order. - Document `generateReusableSchemas` in `docs/content/docs/reference/configuration/output.mdx` (per @melloware's review).
- Stop early-returning at `$ref` in `collectRefsInValue`: OpenAPI 3.1
allows siblings with their own refs; only skip descending into the
`$ref` string itself.
- Integrate back-edge detection into Tarjan and use the SCC's natural
pop order for emit ordering. The previous code lex-sorted nodes
within an SCC, which could leave a non-lazy forward edge pointing
to a not-yet-emitted node and trigger a TDZ at module load.
- Restrict the auto-promote-to-zod branch in `write-specs.ts` to
string `schemas:` configs only. Explicit `{ type: 'typescript' }`
configs are kept on the TS path even when `client: 'zod'` + the
flag are set.
- Document the `readOnly` trade-off: with the flag on, shared
component schemas keep `readOnly` properties in request body
validators (previously stripped via `dereference + removeReadOnlyProperties`).
The previous fix in write-specs.ts inferred "unset" by checking
output.fileExtension === '.ts', which couldn't distinguish the
normalizer default from a user-set '.ts'. Move the default into
normalization where we still see the raw outputOptions: when the
output is zod-schemas (explicit `{ type: 'zod' }` OR a string
`schemas:` + `client: 'zod'` + `generateReusableSchemas`), default
fileExtension to `.zod.ts`; otherwise '.ts'. The write-specs.ts
branch now reads output.fileExtension directly with no override.
A user-set fileExtension still wins via the `?? defaultFileExtension`
chain.
- Scope the `.zod.ts` default to schema writers only: introduce `output.schemaFileExtension` so non-schema writers (client / mock / workspace barrel) keep their own extensions and don't leak the zod suffix when no `fileExtension` is set. `output.fileExtension` keeps its `.ts` default. User-set `fileExtension` still flows to both. - Unify ref naming between the generator and the orchestrator: the orchestrator's `lastRefSegment` now URL-decodes and unescapes JSON Pointer tokens (`~1`/`~0`), matching `getRefInfo(...).originalName` used by the namedRef emission. Avoids drift on refs with escaped characters. - Use `constNameRegistry` for ref-default const names. Without it, multiple defaults sharing the same `name` would collide on `<name>Default`. Matches the suffix pattern used elsewhere in `generateZodValidationSchemaDefinition`. - Move the stray `rewriteReusableSchemas` import in `reusable-schemas.test.ts` up to the main import block. - Switch `collectReachableComponentRefs` BFS from `queue.shift()` to index-based iteration — O(n) instead of O(n²) for large specs. Also update the various test-utils fixtures that build `NormalizedOutputOptions` literals to include the new `schemaFileExtension` field.
- Honor an explicit `output.schemaFileExtension` user-set value during normalization. Added the field to `OutputOptions` and prioritised it over `outputOptions.fileExtension` and the default. This lets users set a separate extension for schema files (e.g. `.ts` for client + `.zod.ts` for schemas) without affecting the global one. - Fix `.default().optional()` ordering in the reusable-ref path. In zod, `.optional()` after `.default()` short-circuits on undefined and the default never runs. Reordered the ref-branch to match the main path (nullable/nullish/optional → default → describe) and skip `.optional()` when a default is set. - Expand the orchestrator's root set to the transitive closure of reachable component-schema refs. Without this, a generated schema could emit a sentinel for a ref not in the seed list, producing an unresolved identifier in the rewritten output. Tests added: - skip .optional() when default is set - emit .nullish() when nullable + !required (combines nullable+optional) - transitive-closure expansion in generateReusableSchemaSet
aff60b3 to
49ee638
Compare
…feat/reusable-zod-schemas
- Emit an explicit `;` after `export const <name> = ${zod}` in both
reusable schema writers. The output already round-tripped fine via
prettier (which inserts the semicolon at the end of the chain), but
emitting it in the raw string keeps the output consistent with other
generators and avoids any ASI edge cases when no formatter is run.
- Update the docs to describe the new `schemaFileExtension` option:
added a top-level `## schemaFileExtension` section explaining when to
use it vs `fileExtension`, and updated the reusable-schemas bullet to
point at it.
|
This also fixed #3068 |
Summary
Fix #3068
Adds an opt-in
override.zod.generateReusableSchemasflag. When enabled, the Zod generator emits one reusable schema per#/components/schemas/*$ref(withnamingConventionapplied to the name) and references it everywhere instead of inlining. Cycles getz.lazy(() => Name)only at the edges that close them.Default is
false; existing output is unchanged when the flag is unset.Behavior
$refto#/components/schemas/X→ an exportedconst X = zod...(orPet,pet_owner, etc., pernamingConvention); cross-schema references use bare identifiers, cycle-closing edges becomezod.lazy(() => X).PetCreateBody.zod.ts) are skipped when the body/response is a pure$ref— consumers import the component schema directly.$ref:nullable,default,description) →Pet.nullable().describe(...)chain.optional→.optional()when the parent doesn't list the property inrequired.properties,example, etc.) → falls back to inlining at that one site, with a verbose log.schemas: 'src/gen/models') ANDclient: 'zod'AND the flag is on, the schemas dir is treated as the zod output (Zod schemas land there, not TypeScript types). Use the explicit{ path, type: 'zod' }form, or rely on this auto-promotion.Implementation
packages/core/src/types.ts: addsgenerateReusableSchemas?: booleantoZodOptionsand required field toNormalizedZodOptions.packages/orval/src/utils/options.ts: defaults the flag tofalsein both normalization sites.packages/zod/src/index.ts:generateZodValidationSchemaDefinitionwidened to acceptOpenApiReferenceObject. New top-of-function branch emits anamedRefplaceholder + chained modifiers when the input is a plain component-schema$ref. Non-chainable siblings fall back to today'sdereferencepath at that one site.parseZodValidationSchemaDefinitionreturnsusedRefs: Set<string>and renders refs as__REF_<name>__sentinels.generateZodRoutecollectsusedRefsfrom each parser call and rewrites the operation-level output's sentinels to bare names (operations are top-of-graph and never need lazy).generateZodreturns the refs asimportswithvalues: trueso mode writers emit properimport { ... } from '...'lines.packages/orval/src/reusable-schemas.ts(new): orchestrator that walks the spec, runs the generator per root, builds the ref dependency graph, runs Tarjan's SCC + DFS back-edge detection, rewrites sentinels into direct refs orzod.lazy(...), and emits in topological order.packages/orval/src/write-zod-specs.ts:writeZodSchemasandgenerateZodSchemasInlineroute through the orchestrator when the flag is on.writeZodSchemasFromVerbsskips pure-ref wrappers.packages/orval/src/write-specs.ts: whenclient: 'zod'+ flag is on +schemas:is a plain string, treat the schemas dir as zod (auto-promotion to the zod writer).Test plan
generateZodValidationSchemaDefinitioncover plain refs, all chainable siblings, parent-driven optional, and the non-chainable fall-back.parseZodValidationSchemaDefinitioncover__REF__sentinel emission andusedRefsreporting.reusable-schemas.test.ts): name resolution + conflict guard, ref collection, per-root generation with sentinels, Tarjan SCC + back-edge cases (DAG, simple cycle, self-loop, multi-node SCC), and sentinel rewriting + topological order.write-zod-specs.test.tsfor cross-file imports (schemas: { type: 'zod' }), wrapper-skip for pure refs, and inline orchestrator output.samples/swr-with-zod's newpetstoreZodReusableconfig (uses-reusable.zod.tsfile extension so its output sits alongside the existing TS petstore + plain zod outputs without colliding).🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Tests
Documentation
Samples
Chores