UNPKG

next-openapi-gen

Version:

Automatically generate OpenAPI 3.0 documentation from Next.js projects, with support for Zod schemas and TypeScript types.

1,019 lines (1,018 loc) 74.5 kB
import fs from "fs"; import path from "path"; import traverseModule from "@babel/traverse"; import * as t from "@babel/types"; // Handle both ES modules and CommonJS const traverse = traverseModule.default || traverseModule; import { parseTypeScriptFile } from "./utils.js"; import { logger } from "./logger.js"; import { DrizzleZodProcessor } from "./drizzle-zod-processor.js"; /** * Class for converting Zod schemas to OpenAPI specifications */ export class ZodSchemaConverter { schemaDir; zodSchemas = {}; processingSchemas = new Set(); processedModules = new Set(); typeToSchemaMapping = {}; drizzleZodImports = new Set(); constructor(schemaDir) { this.schemaDir = path.resolve(schemaDir); } /** * Find a Zod schema by name and convert it to OpenAPI spec */ convertZodSchemaToOpenApi(schemaName) { // Run pre-scan only one time if (Object.keys(this.typeToSchemaMapping).length === 0) { this.preScanForTypeMappings(); } logger.debug(`Looking for Zod schema: ${schemaName}`); // Check mapped types const mappedSchemaName = this.typeToSchemaMapping[schemaName]; if (mappedSchemaName) { logger.debug(`Type '${schemaName}' is mapped to schema '${mappedSchemaName}'`); schemaName = mappedSchemaName; } // Check for circular references if (this.processingSchemas.has(schemaName)) { return { $ref: `#/components/schemas/${schemaName}` }; } // Add to processing set this.processingSchemas.add(schemaName); try { // Return cached schema if it exists if (this.zodSchemas[schemaName]) { return this.zodSchemas[schemaName]; } // Find all route files and process them first const routeFiles = this.findRouteFiles(); for (const routeFile of routeFiles) { this.processFileForZodSchema(routeFile, schemaName); if (this.zodSchemas[schemaName]) { logger.debug(`Found Zod schema '${schemaName}' in route file: ${routeFile}`); return this.zodSchemas[schemaName]; } } // Scan schema directory this.scanDirectoryForZodSchema(this.schemaDir, schemaName); // Return the schema if found, or null if not if (this.zodSchemas[schemaName]) { logger.debug(`Found and processed Zod schema: ${schemaName}`); return this.zodSchemas[schemaName]; } logger.debug(`Could not find Zod schema: ${schemaName}`); return null; } finally { // Remove from processing set this.processingSchemas.delete(schemaName); } } /** * Find all route files in the project */ findRouteFiles() { const routeFiles = []; // Look for route files in common Next.js API directories const possibleApiDirs = [ path.join(process.cwd(), "src", "app", "api"), path.join(process.cwd(), "src", "pages", "api"), path.join(process.cwd(), "app", "api"), path.join(process.cwd(), "pages", "api"), ]; for (const dir of possibleApiDirs) { if (fs.existsSync(dir)) { this.findRouteFilesInDir(dir, routeFiles); } } return routeFiles; } /** * Recursively find route files in a directory */ findRouteFilesInDir(dir, routeFiles) { try { const files = fs.readdirSync(dir); for (const file of files) { const filePath = path.join(dir, file); const stats = fs.statSync(filePath); if (stats.isDirectory()) { this.findRouteFilesInDir(filePath, routeFiles); } else if (file === "route.ts" || file === "route.tsx" || (file.endsWith(".ts") && file.includes("api"))) { routeFiles.push(filePath); } } } catch (error) { logger.error(`Error scanning directory ${dir} for route files: ${error}`); } } /** * Recursively scan directory for Zod schemas */ scanDirectoryForZodSchema(dir, schemaName) { try { const files = fs.readdirSync(dir); for (const file of files) { const filePath = path.join(dir, file); const stats = fs.statSync(filePath); if (stats.isDirectory()) { this.scanDirectoryForZodSchema(filePath, schemaName); } else if (file.endsWith(".ts") || file.endsWith(".tsx")) { this.processFileForZodSchema(filePath, schemaName); } } } catch (error) { logger.error(`Error scanning directory ${dir}: ${error}`); } } /** * Process a file to find Zod schema definitions */ processFileForZodSchema(filePath, schemaName) { try { const content = fs.readFileSync(filePath, "utf-8"); // Check if file contains schema we are looking for if (!content.includes(schemaName)) { return; } // Pre-process all schemas in file this.preprocessAllSchemasInFile(filePath); // Return it, if the schema has already been processed during pre-processing if (this.zodSchemas[schemaName]) { return; } // Parse the file const ast = parseTypeScriptFile(content); // Create a map to store imported modules const importedModules = {}; // Look for all exported Zod schemas traverse(ast, { // Track imports for resolving local and imported schemas ImportDeclaration: (path) => { // Keep track of imports to resolve external schemas const source = path.node.source.value; // Track drizzle-zod imports if (source === "drizzle-zod") { path.node.specifiers.forEach((specifier) => { if (t.isImportSpecifier(specifier) || t.isImportDefaultSpecifier(specifier)) { this.drizzleZodImports.add(specifier.local.name); } }); } // Process each import specifier path.node.specifiers.forEach((specifier) => { if (t.isImportSpecifier(specifier) || t.isImportDefaultSpecifier(specifier)) { const importedName = specifier.local.name; importedModules[importedName] = source; } }); }, // For export const SchemaName = z.object({...}) ExportNamedDeclaration: (path) => { if (t.isVariableDeclaration(path.node.declaration)) { path.node.declaration.declarations.forEach((declaration) => { if (t.isIdentifier(declaration.id) && declaration.id.name === schemaName && declaration.init) { // Check if this is a drizzle-zod helper function if (t.isCallExpression(declaration.init) && t.isIdentifier(declaration.init.callee) && this.drizzleZodImports.has(declaration.init.callee.name)) { const schema = this.processZodNode(declaration.init); if (schema) { this.zodSchemas[schemaName] = schema; } } // Check if this is a call expression with .extend() else if (t.isCallExpression(declaration.init) && t.isMemberExpression(declaration.init.callee) && t.isIdentifier(declaration.init.callee.property) && declaration.init.callee.property.name === "extend") { const schema = this.processZodNode(declaration.init); if (schema) { this.zodSchemas[schemaName] = schema; } } // Existing code for z.object({...}) else if (t.isCallExpression(declaration.init) && t.isMemberExpression(declaration.init.callee) && t.isIdentifier(declaration.init.callee.object) && declaration.init.callee.object.name === "z") { const schema = this.processZodNode(declaration.init); if (schema) { this.zodSchemas[schemaName] = schema; } } } }); } else if (t.isTSTypeAliasDeclaration(path.node.declaration)) { // Handle export type aliases with z schema definitions if (t.isIdentifier(path.node.declaration.id) && path.node.declaration.id.name === schemaName) { const typeAnnotation = path.node.declaration.typeAnnotation; // Check if this is a reference to a z schema (e.g., export type UserSchema = z.infer<typeof UserSchema>) if (t.isTSTypeReference(typeAnnotation) && t.isIdentifier(typeAnnotation.typeName) && typeAnnotation.typeName.name === "z.infer") { // Extract the schema name from z.infer<typeof SchemaName> if (typeAnnotation.typeParameters && typeAnnotation.typeParameters.params.length > 0 && t.isTSTypeReference(typeAnnotation.typeParameters.params[0]) && t.isTSTypeQuery(typeAnnotation.typeParameters.params[0].typeName) && t.isIdentifier( // @ts-ignore typeAnnotation.typeParameters.params[0].typeName.exprName)) { const referencedSchema = // @ts-ignore typeAnnotation.typeParameters.params[0].typeName.exprName .name; // Look for the referenced schema in the same file if (!this.zodSchemas[referencedSchema]) { this.processFileForZodSchema(filePath, referencedSchema); } // Use the referenced schema for this type alias if (this.zodSchemas[referencedSchema]) { this.zodSchemas[schemaName] = this.zodSchemas[referencedSchema]; } } } } } }, // For const SchemaName = z.object({...}) VariableDeclarator: (path) => { if (t.isIdentifier(path.node.id) && path.node.id.name === schemaName && path.node.init) { // Check if this is any Zod schema (including chained calls) if (this.isZodSchema(path.node.init)) { const schema = this.processZodNode(path.node.init); if (schema) { this.zodSchemas[schemaName] = schema; } return; } // Helper function for processing the call chain const processChainedCall = (node, baseSchema) => { if (!t.isCallExpression(node) || !t.isMemberExpression(node.callee)) { return baseSchema; } // @ts-ignore const methodName = node.callee.property.name; let schema = baseSchema; // If there is an even deeper call, process it first if (t.isCallExpression(node.callee.object)) { schema = processChainedCall(node.callee.object, baseSchema); } // Now apply the current method switch (methodName) { case "omit": if (node.arguments.length > 0 && t.isObjectExpression(node.arguments[0])) { node.arguments[0].properties.forEach((prop) => { if (t.isObjectProperty(prop) && t.isBooleanLiteral(prop.value) && prop.value.value === true) { const key = t.isIdentifier(prop.key) ? prop.key.name : t.isStringLiteral(prop.key) ? prop.key.value : null; if (key && schema.properties) { logger.debug(`Removing property: ${key}`); delete schema.properties[key]; if (schema.required) { schema.required = schema.required.filter((r) => r !== key); } } } }); } break; case "partial": // All fields become optional if (schema.properties) { Object.keys(schema.properties).forEach((key) => { schema.properties[key].nullable = true; }); // Remove all required delete schema.required; } break; case "pick": if (node.arguments.length > 0 && t.isObjectExpression(node.arguments[0])) { const keysToPick = []; node.arguments[0].properties.forEach((prop) => { if (t.isObjectProperty(prop) && t.isBooleanLiteral(prop.value) && prop.value.value === true) { const key = t.isIdentifier(prop.key) ? prop.key.name : t.isStringLiteral(prop.key) ? prop.key.value : null; if (key) keysToPick.push(key); } }); // Keep only selected properties if (schema.properties) { const newProperties = {}; keysToPick.forEach((key) => { if (schema.properties[key]) { newProperties[key] = schema.properties[key]; } }); schema.properties = newProperties; // Update required if (schema.required) { schema.required = schema.required.filter((key) => keysToPick.includes(key)); } } } break; case "required": // All fields become required if (schema.properties) { const requiredFields = Object.keys(schema.properties); schema.required = requiredFields; // Remove nullable from fields Object.keys(schema.properties).forEach((key) => { delete schema.properties[key].nullable; }); } break; } return schema; }; // Find the underlying schema (the most nested object in the chain) const findBaseSchema = (node) => { if (t.isIdentifier(node)) { return node.name; } else if (t.isMemberExpression(node)) { return findBaseSchema(node.object); } else if (t.isCallExpression(node) && t.isMemberExpression(node.callee)) { return findBaseSchema(node.callee.object); } return null; }; // Check method calls on other schemas if (t.isCallExpression(path.node.init)) { const baseSchemaName = findBaseSchema(path.node.init); if (baseSchemaName && baseSchemaName !== "z") { logger.debug(`Found chained call starting from: ${baseSchemaName}`); // First make sure the underlying schema is processed if (!this.zodSchemas[baseSchemaName]) { logger.debug(`Base schema ${baseSchemaName} not found, processing it first`); this.processFileForZodSchema(filePath, baseSchemaName); } if (this.zodSchemas[baseSchemaName]) { logger.debug("Base schema found, applying transformations"); // Copy base schema const baseSchema = JSON.parse(JSON.stringify(this.zodSchemas[baseSchemaName])); // Process the entire call chain const finalSchema = processChainedCall(path.node.init, baseSchema); this.zodSchemas[schemaName] = finalSchema; logger.debug(`Created ${schemaName} with properties: ${Object.keys(finalSchema.properties || {})}`); return; } } } // Check if it is .extend() if (t.isCallExpression(path.node.init) && t.isMemberExpression(path.node.init.callee) && t.isIdentifier(path.node.init.callee.property) && path.node.init.callee.property.name === "extend") { const schema = this.processZodNode(path.node.init); if (schema) { this.zodSchemas[schemaName] = schema; } } // Existing code else { const schema = this.processZodNode(path.node.init); if (schema) { this.zodSchemas[schemaName] = schema; } } } }, // For type aliases that reference Zod schemas TSTypeAliasDeclaration: (path) => { if (t.isIdentifier(path.node.id)) { const typeName = path.node.id.name; if (t.isTSTypeReference(path.node.typeAnnotation) && t.isTSQualifiedName(path.node.typeAnnotation.typeName) && t.isIdentifier(path.node.typeAnnotation.typeName.left) && path.node.typeAnnotation.typeName.left.name === "z" && t.isIdentifier(path.node.typeAnnotation.typeName.right) && path.node.typeAnnotation.typeName.right.name === "infer") { // Extract schema name from z.infer<typeof SchemaName> if (path.node.typeAnnotation.typeParameters && path.node.typeAnnotation.typeParameters.params.length > 0) { const param = path.node.typeAnnotation.typeParameters.params[0]; if (t.isTSTypeQuery(param) && t.isIdentifier(param.exprName)) { const referencedSchemaName = param.exprName.name; // Save mapping: TypeName -> SchemaName this.typeToSchemaMapping[typeName] = referencedSchemaName; logger.debug(`Mapped type '${typeName}' to schema '${referencedSchemaName}'`); // Process the referenced schema if not already processed if (!this.zodSchemas[referencedSchemaName]) { this.processFileForZodSchema(filePath, referencedSchemaName); } // Use the referenced schema for this type if (this.zodSchemas[referencedSchemaName]) { this.zodSchemas[typeName] = this.zodSchemas[referencedSchemaName]; } } } } if (path.node.id.name === schemaName) { // Try to find if this is a z.infer<typeof SchemaName> pattern if (t.isTSTypeReference(path.node.typeAnnotation) && t.isIdentifier(path.node.typeAnnotation.typeName) && path.node.typeAnnotation.typeName.name === "infer" && path.node.typeAnnotation.typeParameters && path.node.typeAnnotation.typeParameters.params.length > 0) { const param = path.node.typeAnnotation.typeParameters.params[0]; if (t.isTSTypeQuery(param) && t.isIdentifier(param.exprName)) { const referencedSchemaName = param.exprName.name; // Find the referenced schema this.processFileForZodSchema(filePath, referencedSchemaName); if (this.zodSchemas[referencedSchemaName]) { this.zodSchemas[schemaName] = this.zodSchemas[referencedSchemaName]; } } } } } }, }); } catch (error) { logger.error(`Error processing file ${filePath} for schema ${schemaName}: ${error}`); } } /** * Process all exported schemas in a file, not just the one we're looking for */ processAllSchemasInFile(filePath) { try { const content = fs.readFileSync(filePath, "utf-8"); const ast = parseTypeScriptFile(content); traverse(ast, { ExportNamedDeclaration: (path) => { if (t.isVariableDeclaration(path.node.declaration)) { path.node.declaration.declarations.forEach((declaration) => { if (t.isIdentifier(declaration.id) && declaration.init && t.isCallExpression(declaration.init) && t.isMemberExpression(declaration.init.callee) && t.isIdentifier(declaration.init.callee.object) && declaration.init.callee.object.name === "z") { const schemaName = declaration.id.name; if (!this.zodSchemas[schemaName] && !this.processingSchemas.has(schemaName)) { this.processingSchemas.add(schemaName); const schema = this.processZodNode(declaration.init); if (schema) { this.zodSchemas[schemaName] = schema; } this.processingSchemas.delete(schemaName); } } }); } }, }); } catch (error) { logger.error(`Error processing all schemas in file ${filePath}: ${error}`); } } /** * Process a Zod node and convert it to OpenAPI schema */ processZodNode(node) { // Handle drizzle-zod helper functions (e.g., createInsertSchema, createSelectSchema) if (t.isCallExpression(node) && t.isIdentifier(node.callee) && this.drizzleZodImports.has(node.callee.name)) { return DrizzleZodProcessor.processSchema(node); } // Handle reference to another schema (e.g. UserBaseSchema.extend) if (t.isCallExpression(node) && t.isMemberExpression(node.callee) && t.isIdentifier(node.callee.object) && t.isIdentifier(node.callee.property) && node.callee.property.name === "extend") { const baseSchemaName = node.callee.object.name; // Check if the base schema already exists if (!this.zodSchemas[baseSchemaName]) { // Try to find the basic pattern this.convertZodSchemaToOpenApi(baseSchemaName); } return this.processZodChain(node); } // Handle z.coerce.TYPE() patterns if (t.isCallExpression(node) && t.isMemberExpression(node.callee) && t.isMemberExpression(node.callee.object) && t.isIdentifier(node.callee.object.object) && node.callee.object.object.name === "z" && t.isIdentifier(node.callee.object.property) && node.callee.object.property.name === "coerce" && t.isIdentifier(node.callee.property)) { const coerceType = node.callee.property.name; // Create a synthetic node for the underlying type using Babel types const syntheticNode = t.callExpression(t.memberExpression(t.identifier("z"), t.identifier(coerceType)), []); return this.processZodPrimitive(syntheticNode); } // Handle z.object({...}) if (t.isCallExpression(node) && t.isMemberExpression(node.callee) && t.isIdentifier(node.callee.object) && node.callee.object.name === "z" && t.isIdentifier(node.callee.property)) { const methodName = node.callee.property.name; if (methodName === "object" && node.arguments.length > 0) { return this.processZodObject(node); } else if (methodName === "union" && node.arguments.length > 0) { return this.processZodUnion(node); } else if (methodName === "intersection" && node.arguments.length > 0) { return this.processZodIntersection(node); } else if (methodName === "tuple" && node.arguments.length > 0) { return this.processZodTuple(node); } else if (methodName === "discriminatedUnion" && node.arguments.length > 1) { return this.processZodDiscriminatedUnion(node); } else if (methodName === "literal" && node.arguments.length > 0) { return this.processZodLiteral(node); } else { return this.processZodPrimitive(node); } } // Handle schema reference with method calls, e.g., Image.optional(), UserSchema.nullable() if (t.isCallExpression(node) && t.isMemberExpression(node.callee) && t.isIdentifier(node.callee.object) && t.isIdentifier(node.callee.property) && node.callee.object.name !== "z" // Make sure it's not a z.* call ) { const schemaName = node.callee.object.name; const methodName = node.callee.property.name; // Process base schema first if not already processed if (!this.zodSchemas[schemaName]) { this.convertZodSchemaToOpenApi(schemaName); } // If the schema exists, create a reference and apply the method if (this.zodSchemas[schemaName]) { let schema = { allOf: [{ $ref: `#/components/schemas/${schemaName}` }], }; // Apply method-specific transformations switch (methodName) { case "optional": case "nullable": case "nullish": // Don't add nullable flag here as it would be at the wrong level // The fact that it's optional is handled by not including it in required array break; case "describe": if (node.arguments.length > 0 && t.isStringLiteral(node.arguments[0])) { schema.description = node.arguments[0].value; } break; default: // For other methods, process as a chain return this.processZodChain(node); } return schema; } } // Handle chained methods, e.g., z.string().email().min(5) if (t.isCallExpression(node) && t.isMemberExpression(node.callee) && t.isCallExpression(node.callee.object)) { return this.processZodChain(node); } // Handle schema references like z.lazy(() => AnotherSchema) if (t.isCallExpression(node) && t.isMemberExpression(node.callee) && t.isIdentifier(node.callee.object) && node.callee.object.name === "z" && t.isIdentifier(node.callee.property) && node.callee.property.name === "lazy" && node.arguments.length > 0) { return this.processZodLazy(node); } logger.debug("Unknown Zod schema node:", node); return { type: "object" }; } /** * Process a Zod lazy schema: z.lazy(() => Schema) */ processZodLazy(node) { // Get the function in z.lazy(() => Schema) if (node.arguments.length > 0 && t.isArrowFunctionExpression(node.arguments[0]) && node.arguments[0].body) { const returnExpr = node.arguments[0].body; // If the function returns an identifier, it's likely a reference to another schema if (t.isIdentifier(returnExpr)) { const schemaName = returnExpr.name; // Create a reference to the schema return { $ref: `#/components/schemas/${schemaName}` }; } // If the function returns a complex expression, try to process it return this.processZodNode(returnExpr); } return { type: "object" }; } /** * Process a Zod literal schema: z.literal("value") */ processZodLiteral(node) { if (node.arguments.length === 0) { return { type: "string" }; } const arg = node.arguments[0]; if (t.isStringLiteral(arg)) { return { type: "string", enum: [arg.value], }; } else if (t.isNumericLiteral(arg)) { return { type: "number", enum: [arg.value], }; } else if (t.isBooleanLiteral(arg)) { return { type: "boolean", enum: [arg.value], }; } return { type: "string" }; } /** * Process a Zod discriminated union: z.discriminatedUnion("type", [schema1, schema2]) */ processZodDiscriminatedUnion(node) { if (node.arguments.length < 2) { return { type: "object" }; } // Get the discriminator field name let discriminator = ""; if (t.isStringLiteral(node.arguments[0])) { discriminator = node.arguments[0].value; } // Get the schemas array const schemasArray = node.arguments[1]; if (!t.isArrayExpression(schemasArray)) { return { type: "object" }; } const schemas = schemasArray.elements .map((element) => this.processZodNode(element)) .filter((schema) => schema !== null); if (schemas.length === 0) { return { type: "object" }; } // Create a discriminated mapping for oneOf return { type: "object", discriminator: discriminator ? { propertyName: discriminator, } : undefined, oneOf: schemas, }; } /** * Process a Zod tuple schema: z.tuple([z.string(), z.number()]) */ processZodTuple(node) { if (node.arguments.length === 0 || !t.isArrayExpression(node.arguments[0])) { return { type: "array", items: { type: "string" } }; } const tupleItems = node.arguments[0].elements.map((element) => this.processZodNode(element)); // In OpenAPI, we can represent this as an array with prefixItems (OpenAPI 3.1+) // For OpenAPI 3.0.x, we'll use items with type: array return { type: "array", items: tupleItems.length > 0 ? tupleItems[0] : { type: "string" }, // For OpenAPI 3.1+: prefixItems: tupleItems }; } /** * Process a Zod intersection schema: z.intersection(schema1, schema2) */ processZodIntersection(node) { if (node.arguments.length < 2) { return { type: "object" }; } const schema1 = this.processZodNode(node.arguments[0]); const schema2 = this.processZodNode(node.arguments[1]); // In OpenAPI, we can use allOf to represent intersection return { allOf: [schema1, schema2], }; } /** * Process a Zod union schema: z.union([schema1, schema2]) */ processZodUnion(node) { if (node.arguments.length === 0 || !t.isArrayExpression(node.arguments[0])) { return { type: "object" }; } const unionItems = node.arguments[0].elements.map((element) => this.processZodNode(element)); // Check for common pattern: z.union([z.string(), z.null()]) which should be nullable string if (unionItems.length === 2) { const isNullable = unionItems.some((item) => item.type === "null" || (item.enum && item.enum.length === 1 && item.enum[0] === null)); if (isNullable) { const nonNullItem = unionItems.find((item) => item.type !== "null" && !(item.enum && item.enum.length === 1 && item.enum[0] === null)); if (nonNullItem) { return { ...nonNullItem, nullable: true, }; } } } // Check if all union items are of the same type with different enum values // This is common for string literals like: z.union([z.literal("a"), z.literal("b")]) const allSameType = unionItems.length > 0 && unionItems.every((item) => item.type === unionItems[0].type && item.enum); if (allSameType) { // Combine all enum values const combinedEnums = unionItems.flatMap((item) => item.enum || []); return { type: unionItems[0].type, enum: combinedEnums, }; } // Otherwise, use oneOf for general unions return { oneOf: unionItems, }; } /** * Process a Zod object schema: z.object({...}) */ processZodObject(node) { if (node.arguments.length === 0 || !t.isObjectExpression(node.arguments[0])) { return { type: "object" }; } const objectExpression = node.arguments[0]; const properties = {}; const required = []; objectExpression.properties.forEach((prop, index) => { if (t.isObjectProperty(prop)) { let propName; // Handle both identifier and string literal keys if (t.isIdentifier(prop.key)) { propName = prop.key.name; } else if (t.isStringLiteral(prop.key)) { propName = prop.key.value; } else { logger.debug(`Skipping property ${index} - unsupported key type`); return; // Skip if key is not identifier or string literal } if (t.isCallExpression(prop.value) && t.isMemberExpression(prop.value.callee) && t.isIdentifier(prop.value.callee.object)) { const schemaName = prop.value.callee.object.name; // @ts-ignore const methodName = prop.value.callee.property.name; // Process base schema first if (!this.zodSchemas[schemaName]) { this.convertZodSchemaToOpenApi(schemaName); } // For describe method, use reference with description if (methodName === "describe" && this.zodSchemas[schemaName]) { if (prop.value.arguments.length > 0 && t.isStringLiteral(prop.value.arguments[0])) { properties[propName] = { allOf: [{ $ref: `#/components/schemas/${schemaName}` }], description: prop.value.arguments[0].value, }; } else { properties[propName] = { $ref: `#/components/schemas/${schemaName}`, }; } required.push(propName); return; } // For other methods, process normally const processedSchema = this.processZodNode(prop.value); if (processedSchema) { properties[propName] = processedSchema; const isOptional = this.isOptional(prop.value) || this.hasOptionalMethod(prop.value); if (!isOptional) { required.push(propName); } } return; } // Check if the property value is an identifier (reference to another schema) if (t.isIdentifier(prop.value)) { const referencedSchemaName = prop.value.name; // Try to find and convert the referenced schema if (!this.zodSchemas[referencedSchemaName]) { this.convertZodSchemaToOpenApi(referencedSchemaName); } // Create a reference properties[propName] = { $ref: `#/components/schemas/${referencedSchemaName}`, }; required.push(propName); // Assuming it's required unless marked optional } // For array of schemas (like z.array(PaymentMethodSchema)) if (t.isCallExpression(prop.value) && t.isMemberExpression(prop.value.callee) && t.isIdentifier(prop.value.callee.object) && prop.value.callee.object.name === "z" && t.isIdentifier(prop.value.callee.property) && prop.value.callee.property.name === "array" && prop.value.arguments.length > 0 && t.isIdentifier(prop.value.arguments[0])) { const itemSchemaName = prop.value.arguments[0].name; // Try to find and convert the referenced schema if (!this.zodSchemas[itemSchemaName]) { this.convertZodSchemaToOpenApi(itemSchemaName); } // Process as array with reference const arraySchema = this.processZodNode(prop.value); arraySchema.items = { $ref: `#/components/schemas/${itemSchemaName}`, }; properties[propName] = arraySchema; const isOptional = this.isOptional(prop.value) || this.hasOptionalMethod(prop.value); if (!isOptional) { required.push(propName); } } // Process property value (a Zod schema) const propSchema = this.processZodNode(prop.value); if (propSchema) { properties[propName] = propSchema; // If the property is not marked as optional, add it to required list const isOptional = // @ts-ignore this.isOptional(prop.value) || this.hasOptionalMethod(prop.value); if (!isOptional) { required.push(propName); } } } }); const schema = { type: "object", properties, }; if (required.length > 0) { // @ts-ignore schema.required = required; } return schema; } /** * Process a Zod primitive schema: z.string(), z.number(), etc. */ processZodPrimitive(node) { if (!t.isMemberExpression(node.callee) || !t.isIdentifier(node.callee.property)) { return { type: "string" }; } const zodType = node.callee.property.name; let schema = {}; // Basic type mapping switch (zodType) { case "string": schema = { type: "string" }; break; case "number": schema = { type: "number" }; break; case "boolean": schema = { type: "boolean" }; break; case "date": schema = { type: "string", format: "date-time" }; break; case "bigint": schema = { type: "integer", format: "int64" }; break; case "any": case "unknown": schema = {}; // Empty schema matches anything break; case "null": case "undefined": schema = { type: "null" }; break; case "array": let itemsType = { type: "string" }; if (node.arguments.length > 0) { // Check if argument is an identifier (schema reference) if (t.isIdentifier(node.arguments[0])) { const schemaName = node.arguments[0].name; // Try to find and convert the referenced schema if (!this.zodSchemas[schemaName]) { this.convertZodSchemaToOpenApi(schemaName); } // @ts-ignore itemsType = { $ref: `#/components/schemas/${schemaName}` }; } else { // @ts-ignore itemsType = this.processZodNode(node.arguments[0]); } } schema = { type: "array", items: itemsType }; break; case "enum": if (node.arguments.length > 0 && t.isArrayExpression(node.arguments[0])) { const enumValues = node.arguments[0].elements .filter((el) => t.isStringLiteral(el) || t.isNumericLiteral(el)) // @ts-ignore .map((el) => el.value); const firstValue = enumValues[0]; const valueType = typeof firstValue; schema = { type: valueType === "number" ? "number" : "string", enum: enumValues, }; } else if (node.arguments.length > 0 && t.isObjectExpression(node.arguments[0])) { // Handle z.enum({ KEY1: "value1", KEY2: "value2" }) const enumValues = []; node.arguments[0].properties.forEach((prop) => { if (t.isObjectProperty(prop) && t.isStringLiteral(prop.value)) { enumValues.push(prop.value.value); } }); if (enumValues.length > 0) { schema = { type: "string", enum: enumValues, }; } else { schema = { type: "string" }; } } else { schema = { type: "string" }; } break; case "record": let valueType = { type: "string" }; if (node.arguments.length > 0) { valueType = this.processZodNode(node.arguments[0]); } schema = { type: "object", additionalProperties: valueType, }; break;