UNPKG

@techery/zod-to-openai-schema

Version:

Convert Zod schemas to OpenAI structured output compatible schemas

232 lines 8.63 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.definition = definition; exports.zodToOpenAISchema = zodToOpenAISchema; const zod_1 = require("zod"); const MAX_DEPTH = 100; /** * 1) Collect usage counts for all Zod nodes within "rootSchema". */ function collectUsageCounts(rootSchema) { const usageCount = new Map(); function visit(schema, depth) { var _a, _b; if (depth > MAX_DEPTH) { return; } usageCount.set(schema, (usageCount.get(schema) || 0) + 1); const def = schema === null || schema === void 0 ? void 0 : schema._def; const typeName = def === null || def === void 0 ? void 0 : def.typeName; switch (typeName) { case 'ZodObject': { const shape = def.shape(); for (const key of Object.keys(shape)) { visit(shape[key], depth + 1); } break; } case 'ZodArray': { visit(def.type, depth + 1); break; } case 'ZodUnion': { (def.options || []).forEach((option) => visit(option, depth + 1)); break; } case 'ZodDiscriminatedUnion': { for (const opt of ((_a = def.optionsMap) === null || _a === void 0 ? void 0 : _a.values()) || []) { visit(opt, depth + 1); } break; } case 'ZodLazy': { const inner = def.getter(); visit(inner, depth + 1); break; } case 'ZodDefault': case 'ZodOptional': case 'ZodNullable': { const inner = def.innerType || ((_b = def._def) === null || _b === void 0 ? void 0 : _b.innerType); if (inner) { visit(inner, depth + 1); } break; } // For ZodString, ZodNumber, ZodBoolean, ZodEnum, etc.: no children default: break; } } visit(rootSchema, 0); return usageCount; } function definition(name, schema) { return { name, schema }; } /** * 2) Convert the root Zod schema into JSONSchema: * - If a ZodObject node is repeated (usage>1), place it in $defs and reference it. * - All other node types (array, union, lazy, etc.) are inlined, even if repeated. * - The root schema is never just "$ref". * - additionalProperties=false for objects. */ function zodToOpenAISchema(rootSchema, config = {}) { const usageCount = collectUsageCounts(rootSchema); // Map "ZodObject" => definition name const objectDefs = new Map(); const defs = {}; let defCounter = 1; function build(node, isRoot) { const def = node === null || node === void 0 ? void 0 : node._def; const typeName = def === null || def === void 0 ? void 0 : def.typeName; // Only store repeated ZodObject nodes in $defs const isObject = typeName === 'ZodObject'; const count = usageCount.get(node) || 0; if (!isRoot && isObject && count > 1) { if (!objectDefs.has(node)) { if (config.definitions) { const existingDef = config.definitions.find((d) => d.schema === node); if (existingDef) { objectDefs.set(node, existingDef.name); } else { objectDefs.set(node, `Def_${defCounter++}`); } } else { objectDefs.set(node, `Def_${defCounter++}`); } } return { $ref: `#/$defs/${objectDefs.get(node)}` }; } // Inline everything else return parseNode(node); } function parseNode(node) { var _a; const def = node === null || node === void 0 ? void 0 : node._def; const typeName = def === null || def === void 0 ? void 0 : def.typeName; const schema = {}; if (node.description) { schema.description = node.description; } switch (typeName) { case 'ZodString': schema.type = 'string'; break; case 'ZodNumber': { const checks = def.checks || []; const isInt = checks.some((c) => c.kind === 'int'); schema.type = isInt ? 'integer' : 'number'; break; } case 'ZodBoolean': schema.type = 'boolean'; break; case 'ZodBigInt': schema.type = 'integer'; break; case 'ZodObject': { schema.type = 'object'; schema.additionalProperties = false; schema.properties = {}; const shape = def.shape(); const requiredKeys = []; for (const key of Object.keys(shape)) { const propSchema = shape[key]; requiredKeys.push(key); if (propSchema.isNullable()) { schema.properties[key] = build(zod_1.z.union([propSchema, zod_1.z.null()]), false); } else { schema.properties[key] = build(propSchema, false); } if (propSchema.isOptional()) { throw new Error('Optional fields are not allowed'); } } if (requiredKeys.length > 0) { schema.required = requiredKeys; } break; } case 'ZodArray': schema.type = 'array'; schema.items = build(def.type, false); break; case 'ZodUnion': schema.anyOf = (def.options || []).map((o) => build(o, false)); break; case 'ZodDiscriminatedUnion': { const optionsMap = def.optionsMap; schema.anyOf = Array.from(optionsMap.values()).map((o) => build(o, false)); break; } case 'ZodLazy': { const inner = def.getter(); return build(inner, false); } case 'ZodEnum': schema.type = 'string'; schema.enum = def.values; break; case 'ZodLiteral': { const litVal = def.value; const valType = typeof litVal; if (valType === 'string') { schema.type = 'string'; schema.enum = [litVal]; } else if (valType === 'number') { schema.type = 'number'; schema.enum = [litVal]; } else if (valType === 'boolean') { schema.type = 'boolean'; schema.enum = [litVal]; } else { // e.g. null schema.type = 'string'; schema.enum = [String(litVal)]; } break; } case 'ZodNull': schema.type = 'null'; break; case 'ZodDefault': case 'ZodOptional': case 'ZodNullable': { const inner = def.innerType || ((_a = def._def) === null || _a === void 0 ? void 0 : _a.innerType); return parseNode(inner); } default: throw new Error(`Unsupported or unknown Zod type: ${typeName}`); } return schema; } // Build the top-level schema const rootJson = build(rootSchema, true); // Fill $defs for repeated objects for (const [node, defName] of objectDefs) { const schemaObj = parseNode(node); defs[defName] = schemaObj; } // Attach $defs if any if (Object.keys(defs).length > 0) { rootJson.$defs = defs; } // If the root is just {"$ref": "..."} => inline if (rootJson.$ref && Object.keys(rootJson).length === 1) { const refName = rootJson.$ref.replace(/^#\/\$defs\//, ''); const realRoot = defs[refName]; if (realRoot) { delete rootJson.$ref; Object.assign(rootJson, realRoot); } } return rootJson; } //# sourceMappingURL=zod-to-openai-schema.js.map