Description
When a Zod schema file imports external schemas and also defines a non-exported base shape that is spread into exported schemas, the spread properties are silently dropped from the generated OpenAPI output. Only the non-spread properties survive.
Minimal Reproduction
schemas/base.ts
import { z } from 'zod'
import { addressSchema } from './addressSchema' // any external import
const personBaseShape = {
name: z.string().describe('Full name'),
age: z.number().int().describe('Age in years'),
}
export const employeeSchema = z
.object({
...personBaseShape,
role: z.string().describe('Job role'),
})
.meta({ id: 'Employee' })
Expected OpenAPI output for Employee:
{
"type": "object",
"properties": {
"name": { "type": "string", "description": "Full name" },
"age": { "type": "integer", "description": "Age in years" },
"role": { "type": "string", "description": "Job role" }
},
"required": ["name", "age", "role"]
}
Actual output:
{
"type": "object",
"properties": {
"role": { "type": "string", "description": "Job role" }
},
"required": ["role"]
}
Root Cause
In preprocessAllSchemasInFile, this.currentFilePath, this.currentAST, and this.currentImports are set once at the start of the function. However, processZodNode can trigger convertZodSchemaToOpenApi → processFileForZodSchema → preprocessAllSchemasInFile(importedFile) for each imported schema. After returning from those recursive calls, this.currentFilePath now points to the last imported file — not the original file.
When the next schema in the original file is processed (e.g. employeeSchema after addressSchema was resolved), resolveConstObjectNode("personBaseShape") searches in the wrong file context and returns null. The spread is silently skipped.
Suggested Fix
Restore this.currentFilePath, this.currentAST, and this.currentImports before each processZodNode call in the traversal handlers of preprocessAllSchemasInFile:
ExportNamedDeclaration: (path) => {
// ...
if (!this.getStoredSchema(schemaName)) {
this.processingSchemas.add(schemaName);
+ this.currentFilePath = filePath;
+ this.currentAST = ast;
+ this.currentImports = importedModules;
const schema = this.processZodNode(declaration.init);
// ...
}
},
VariableDeclaration: (path) => {
// ...
if (!this.getStoredSchema(schemaName) && !this.processingSchemas.has(schemaName)) {
this.processingSchemas.add(schemaName);
+ this.currentFilePath = filePath;
+ this.currentAST = ast;
+ this.currentImports = importedModules;
const schema = this.processZodNode(declaration.init);
// ...
}
},
Environment
Description
When a Zod schema file imports external schemas and also defines a non-exported base shape that is spread into exported schemas, the spread properties are silently dropped from the generated OpenAPI output. Only the non-spread properties survive.
Minimal Reproduction
schemas/base.tsExpected OpenAPI output for
Employee:{ "type": "object", "properties": { "name": { "type": "string", "description": "Full name" }, "age": { "type": "integer", "description": "Age in years" }, "role": { "type": "string", "description": "Job role" } }, "required": ["name", "age", "role"] }Actual output:
{ "type": "object", "properties": { "role": { "type": "string", "description": "Job role" } }, "required": ["role"] }Root Cause
In
preprocessAllSchemasInFile,this.currentFilePath,this.currentAST, andthis.currentImportsare set once at the start of the function. However,processZodNodecan triggerconvertZodSchemaToOpenApi→processFileForZodSchema→preprocessAllSchemasInFile(importedFile)for each imported schema. After returning from those recursive calls,this.currentFilePathnow points to the last imported file — not the original file.When the next schema in the original file is processed (e.g.
employeeSchemaafteraddressSchemawas resolved),resolveConstObjectNode("personBaseShape")searches in the wrong file context and returnsnull. The spread is silently skipped.Suggested Fix
Restore
this.currentFilePath,this.currentAST, andthis.currentImportsbefore eachprocessZodNodecall in the traversal handlers ofpreprocessAllSchemasInFile:Environment
next-openapi-genv1.3.0