fix(zod): emit reusable schemas inline when operations exist + use PascalCase identifiers - #3465
Conversation
With `client: 'zod'` + `generateReusableSchemas: true` and no `schemas:`
dir (e.g. `mode: 'single'`), operations reference component schemas by
name but the component definitions were never emitted. `shouldGenerateZodSchemasInline`
only returned true when there were no operations, so the references
dangled and the output didn't compile (e.g. `zod.array(authorizeResource)`
with no `authorizeResource` definition).
- `shouldGenerateZodSchemasInline` now also returns true when the flag is
on, so component schemas are emitted inline alongside operations.
- Thread an `includeZodImport` flag through `generateZodSchemasInline` /
`generateZodSchemasInlineReusable` / `generateZodSchemaFileContent`:
when operations are present the zod client already emits
`import * as zod from 'zod'`, so the inline block must NOT add a second
`import { z as zod }` (which would redeclare `zod`). When there are no
operations the inline block remains the sole zod import.
Test: end-to-end single-mode + reusable + operations now defines the
referenced component schema inline (before its use), references it by
name, emits exactly one zod import, and leaves no `__REF_` sentinels.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (2)
📝 WalkthroughWalkthroughRefactors reusable-schema naming to use Orval ref info (PascalCase exports) and makes Zod inline-schema emission conditionally include the Zod import when operations are absent; tests and snapshots updated to assert single Zod import, correct named exports, and no unresolved _REF sentinels. ChangesZod Inline Schema Generation with Conditional Imports & Reusable-schema Naming
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes 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.
Adjusts zod schema emission in single-mode generation to support generateReusableSchemas when operations are present, while preventing duplicate zod imports in the concatenated output.
Changes:
- Emit component schemas inline even when operations exist if
override.zod.generateReusableSchemasis enabled. - Add an
includeZodImporttoggle so inline schema generation can omitimport { z as zod }when the client already importszod. - Add a regression test covering single-mode + reusable schemas + operations inline emission and import deduping.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| packages/orval/src/write-zod-specs.ts | Adds includeZodImport plumbing and avoids emitting an empty import block / duplicate zod import in inline output. |
| packages/orval/src/write-specs.ts | Updates inline-generation decision logic for reusable schemas and disables schema-side zod import when operations are present. |
| packages/orval/src/generate-spec.test.ts | Adds regression test for inline reusable schemas + operations in single mode (including single zod import check). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
@z4o4z i noticed this too yesterday when i tried it. Thanks for fixing |
Reusable schema exports used `conventionName(name, namingConvention)` for the identifier, defaulting to camelCase (`authorizeResponse`), which was inconsistent with operation wrappers (`AuthorizeAuthorizeBody` via `pascal()`) and the TS model types (`AuthorizeResponse`) in the same project. Both the generator's `namedRef` emission and the orchestrator's `resolveSchemaName` now use `getRefInfo(ref, context).name` — the exact identifier orval already emits for the TS model type (`pascal` + sanitize + component suffix). `namingConvention` continues to govern file names only (e.g. `authorizeResponse.zod.ts`), consistent with the rest of orval; identifiers are always PascalCase. - Drop the now-moot kebab-case validation (sanitized identifiers are always valid) and the bespoke `lastRefSegment`/`conventionName` paths. - Update zod + orchestrator tests for PascalCase names. - Regenerate swr-with-zod reusable snapshots.
- Extract `includeZodImport = !hasOperations` local at the generateSchemasInline call site so it reads with the comment. - Anchor the inline-ordering test on the operation export name (`export const ListPets`) instead of the incidental `Item = Pet` substring. Note: kept the direct boolean use in `shouldGenerateZodSchemasInline` (not `=== true`): `NormalizedOutputOptions` types the field as a required boolean, so `=== true` trips no-unnecessary-boolean-literal-compare.
Two related fixes for
override.zod.generateReusableSchemas(both surfaced on the same real-world spec). Happy to split if preferred.1. Component schemas not emitted inline when operations exist
With
client: 'zod'+generateReusableSchemas: trueand no separateschemas:dir (e.g.mode: 'single'), operations reference component schemas by name but the component definitions were never emitted — the output didn't compile.shouldGenerateZodSchemasInlineonly returnedtruewhen there were no operations. Without the reusable flag that's correct (operations inline their own schemas), but with it, operations reference the component schemas by name, so the definitions must be emitted inline alongside them.shouldGenerateZodSchemasInlinenow also returnstruewhen the flag is on.includeZodImportflag throughgenerateZodSchemasInline/generateZodSchemasInlineReusable/generateZodSchemaFileContent: when operations are present the zod client already emitsimport * as zod from 'zod', so the inline block must not add a secondimport { z as zod }(which would redeclarezod). With no operations, the inline block stays the sole zod import.2. Reusable schema identifiers were camelCase (inconsistent)
Reusable schema exports used
conventionName(name, namingConvention)for the identifier, defaulting to camelCase (authorizeResponse). That clashed with operation wrappers (AuthorizeAuthorizeBody, viapascal()) and the TS model types (AuthorizeResponse) generated for the same spec.Both the generator's
namedRefemission and the orchestrator'sresolveSchemaNamenow usegetRefInfo(ref, context).name— the exact identifier orval already emits for the TS model type (pascal+ sanitize + component suffix).namingConventioncontinues to govern file names only (e.g.authorizeResponse.zod.ts), consistent with the rest of orval; identifiers are always PascalCase.Before / after (
mode: 'single'):Test plan
generate-spec.test.ts): single-mode + reusable + an operation referencing a component schema → component defined inline (before use), referenced by name, exactly one zod import, no__REF_sentinels.swr-with-zodreusable snapshots regenerated (identifiers now PascalCase; file names unchanged — still camelCase).@orval/zod(183),@orval/orval(117),@orval/core(1933) suites pass.🤖 Generated with Claude Code
Summary by CodeRabbit
Bug Fixes
Refactor
Tests