fix(openapi-zod-ts): assert recursive lazy schemas as z.ZodType<T> - #398
Conversation
Recursive component schemas were emitted with an annotation: const FooSchema: z.ZodType<Foo> = z.lazy(() => z.object({...}).passthrough()). The annotation forces tsc to prove the loose .passthrough() output (which carries an index signature { [x: string]: unknown }) is assignable to the strict generated model Foo. For all-optional recursive cycles, including recursion through an array of $ref or a oneOf-with-null union, older TypeScript or Zod versions cannot prove this and fail with TS2322. It surfaces when --reset-schema re-bootstraps schemas.ts, the only path that re-emits recursive schemas.
Emit the assertion form instead: const FooSchema = z.lazy(() => ...) as z.ZodType<Foo>. An assertion is strictly more permissive than a checked annotation, so it drops the fragile assignability check while keeping z.infer concrete (no unknown regression). Only the recursive branch changes; acyclic schemas stay plain assignments.
Tests assert recursive schemas (self, mutual, through-array, oneOf-null) emit the assertion form and acyclic schemas do not. Regenerated showcase output for canada_holidays (mutually recursive through arrays) reflects the new form.
Closes #397
📝 WalkthroughWalkthroughThe recursive-schema code-generation strategy in ChangesCyclic Schema Assertion Form
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes Possibly related issues
Possibly related PRs
Poem
🚥 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 docstrings
🧪 Generate unit tests (beta)
Comment |
Fallow audit reportFound 4 findings. Dependencies (3)
Duplication (1)
Generated by fallow. |
| }) | ||
|
|
||
| it('non-circular schema does not have a z.ZodType annotation (no regression)', () => { | ||
| it('non-circular schema does not have a z.ZodType annotation or assertion (no regression)', () => { |
There was a problem hiding this comment.
warn fallow/code-duplication: Code clone group 1 (7 lines, 2 instances)
| // passthrough object infers an index signature ({ [x: string]: unknown }) that TS cannot | ||
| // always prove assignable to the strict generated model interface in those toolchains. | ||
| // The assertion form bypasses that check while keeping z.infer concrete and exact. | ||
| return `export const ${safeName}Schema = z.lazy(() => ${schemaToZod(schema)}) as z.ZodType<${modelTypeName}>` |
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/openapi-zod-ts/src/__tests__/generator-schema.test.ts`:
- Around line 269-275: Strengthen the acyclic-schema regression test in
generator-schema.test.ts by matching the full declaration lines for ErrorSchema
and RegionSchema instead of only checking abbreviated substrings. Update the
expectations around the existing schemas assertions so they verify the actual
generated assignment line from the schema generator, ensuring regressions like a
trailing “as z.ZodType<...>” on ErrorSchema or RegionSchema are caught. Use the
existing schema name checks in the test to locate the affected assertions.
🪄 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: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: a307b9ff-ac48-40e8-a6a3-88e94f1f79d6
⛔ Files ignored due to path filters (1)
examples/generated/canada_holidays/schemas.tsis excluded by!**/generated/**
📒 Files selected for processing (3)
packages/openapi-zod-ts/src/__tests__/generator-schema.test.tspackages/openapi-zod-ts/src/__tests__/zod-unit.test.tspackages/openapi-zod-ts/src/plugins/zod.ts
| // Acyclic schemas (Error, Region) remain plain assignments with no annotation or assertion. | ||
| expect(schemas).toContain('ErrorSchema =') | ||
| expect(schemas).not.toContain('ErrorSchema: z.ZodType') | ||
| expect(schemas).not.toContain('ErrorSchema as z.ZodType') | ||
| expect(schemas).toContain('RegionSchema =') | ||
| expect(schemas).not.toContain('RegionSchema: z.ZodType') | ||
| expect(schemas).not.toContain('RegionSchema as z.ZodType') |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Strengthen the acyclic-schema regression check.
Line 272 and Line 275 can still pass if generation regresses to ErrorSchema = ... as z.ZodType<...> / RegionSchema = ... as z.ZodType<...>, because the tested substring never occurs in the real declaration. Match the whole declaration line instead of the abbreviated substring.
Suggested fix
expect(schemas).toContain('ErrorSchema =')
expect(schemas).not.toContain('ErrorSchema: z.ZodType')
- expect(schemas).not.toContain('ErrorSchema as z.ZodType')
+ expect(schemas).not.toMatch(/export const ErrorSchema[^\n]*as z\.ZodType/)
expect(schemas).toContain('RegionSchema =')
expect(schemas).not.toContain('RegionSchema: z.ZodType')
- expect(schemas).not.toContain('RegionSchema as z.ZodType')
+ expect(schemas).not.toMatch(/export const RegionSchema[^\n]*as z\.ZodType/)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Acyclic schemas (Error, Region) remain plain assignments with no annotation or assertion. | |
| expect(schemas).toContain('ErrorSchema =') | |
| expect(schemas).not.toContain('ErrorSchema: z.ZodType') | |
| expect(schemas).not.toContain('ErrorSchema as z.ZodType') | |
| expect(schemas).toContain('RegionSchema =') | |
| expect(schemas).not.toContain('RegionSchema: z.ZodType') | |
| expect(schemas).not.toContain('RegionSchema as z.ZodType') | |
| // Acyclic schemas (Error, Region) remain plain assignments with no annotation or assertion. | |
| expect(schemas).toContain('ErrorSchema =') | |
| expect(schemas).not.toContain('ErrorSchema: z.ZodType') | |
| expect(schemas).not.toMatch(/export const ErrorSchema[^\n]*as z\.ZodType/) | |
| expect(schemas).toContain('RegionSchema =') | |
| expect(schemas).not.toContain('RegionSchema: z.ZodType') | |
| expect(schemas).not.toMatch(/export const RegionSchema[^\n]*as z\.ZodType/) |
🤖 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/openapi-zod-ts/src/__tests__/generator-schema.test.ts` around lines
269 - 275, Strengthen the acyclic-schema regression test in
generator-schema.test.ts by matching the full declaration lines for ErrorSchema
and RegionSchema instead of only checking abbreviated substrings. Update the
expectations around the existing schemas assertions so they verify the actual
generated assignment line from the schema generator, ensuring regressions like a
trailing “as z.ZodType<...>” on ErrorSchema or RegionSchema are caught. Use the
existing schema name checks in the test to locate the affected assertions.
Summary
Asserts recursive lazy schemas generated by openapi-zod-ts are properly typed as
z.ZodType<T>rather than falling back tounknown, ensuring type safety for complex nested OpenAPI structures.Changes
Testing
References
Summary by CodeRabbit