import type { JSONSchema7, JSONSchema7Definition } from "json-schema"; import { sanitizeToolName, toPascalCase, escapeJsDoc, escapeStringLiteral, quoteProp } from "./utils"; export interface ConversionContext { root: JSONSchema7; depth: number; seen: Set; maxDepth: number; } /** * Resolve an internal JSON Pointer $ref (e.g. #/definitions/Foo) against the root schema. * Returns null for external URLs or unresolvable paths. */ function resolveRef( ref: string, root: JSONSchema7 ): JSONSchema7Definition | null { // "#" is a valid self-reference to the root schema if (ref === "#") return root; if (!ref.startsWith("#/")) return null; const segments = ref .slice(2) .split("/") .map((s) => s.replace(/~1/g, "/").replace(/~0/g, "~")); let current: unknown = root; for (const seg of segments) { if (current === null || typeof current !== "object") return null; current = (current as Record)[seg]; if (current === undefined) return null; } // Allow both object schemas and boolean schemas (true = any, false = never) if (typeof current === "boolean") return current; if (current === null || typeof current !== "object") return null; return current as JSONSchema7; } /** * Apply OpenAPI 3.0 `nullable: true` to a type result. */ function applyNullable(result: string, schema: unknown): string { if ( result !== "unknown" && result !== "never" && (schema as Record)?.nullable === true ) { return `${result} | null`; } return result; } /** * Convert a JSON Schema to a TypeScript type string. * This is a direct conversion without going through Zod. */ export function jsonSchemaToTypeString( schema: JSONSchema7Definition, indent: string, ctx: ConversionContext ): string { // Handle boolean schemas if (typeof schema === "boolean") { return schema ? "unknown" : "never"; } // Depth guard if (ctx.depth >= ctx.maxDepth) return "unknown"; // Circular reference guard if (ctx.seen.has(schema)) return "unknown"; ctx.seen.add(schema); const nextCtx: ConversionContext = { ...ctx, depth: ctx.depth + 1 }; try { // Handle $ref if (schema.$ref) { const resolved = resolveRef(schema.$ref, ctx.root); if (!resolved) return "unknown"; return applyNullable( jsonSchemaToTypeString(resolved, indent, nextCtx), schema ); } // Handle anyOf/oneOf (union types) if (schema.anyOf) { const types = schema.anyOf.map((s) => jsonSchemaToTypeString(s, indent, nextCtx) ); return applyNullable(types.join(" | "), schema); } if (schema.oneOf) { const types = schema.oneOf.map((s) => jsonSchemaToTypeString(s, indent, nextCtx) ); return applyNullable(types.join(" | "), schema); } // Handle allOf (intersection types) if (schema.allOf) { const types = schema.allOf.map((s) => jsonSchemaToTypeString(s, indent, nextCtx) ); return applyNullable(types.join(" & "), schema); } // Handle enum if (schema.enum) { if (schema.enum.length === 0) return "never"; const result = schema.enum .map((v) => { if (v === null) return "null"; if (typeof v === "string") return '"' + escapeStringLiteral(v) + '"'; if (typeof v === "object") return JSON.stringify(v) ?? "unknown"; return String(v); }) .join(" | "); return applyNullable(result, schema); } // Handle const if (schema.const !== undefined) { const result = schema.const === null ? "null" : typeof schema.const === "string" ? '"' + escapeStringLiteral(schema.const) + '"' : typeof schema.const === "object" ? (JSON.stringify(schema.const) ?? "unknown") : String(schema.const); return applyNullable(result, schema); } // Handle type const type = schema.type; if (type === "string") return applyNullable("string", schema); if (type === "number" || type === "integer") return applyNullable("number", schema); if (type === "boolean") return applyNullable("boolean", schema); if (type === "null") return "null"; if (type === "array") { // Tuple support: prefixItems (JSON Schema 2020-12) const prefixItems = (schema as Record) .prefixItems as JSONSchema7Definition[]; if (Array.isArray(prefixItems)) { const types = prefixItems.map((s) => jsonSchemaToTypeString(s, indent, nextCtx) ); return applyNullable(`[${types.join(", ")}]`, schema); } // Tuple support: items as array (draft-07) if (Array.isArray(schema.items)) { const types = schema.items.map((s) => jsonSchemaToTypeString(s, indent, nextCtx) ); return applyNullable(`[${types.join(", ")}]`, schema); } if (schema.items) { const itemType = jsonSchemaToTypeString(schema.items, indent, nextCtx); return applyNullable(`${itemType}[]`, schema); } return applyNullable("unknown[]", schema); } if (type === "object" || schema.properties) { const props = schema.properties || {}; const required = new Set(schema.required || []); const lines: string[] = []; for (const [propName, propSchema] of Object.entries(props)) { if (typeof propSchema === "boolean") { const boolType = propSchema ? "unknown" : "never"; const optionalMark = required.has(propName) ? "" : "?"; lines.push( `${indent} ${quoteProp(propName)}${optionalMark}: ${boolType};` ); continue; } const isRequired = required.has(propName); const propType = jsonSchemaToTypeString( propSchema, indent + " ", nextCtx ); const desc = propSchema.description; const format = propSchema.format; if (desc || format) { const descText = desc ? escapeJsDoc(desc.replace(/\r?\n/g, " ")) : undefined; const formatTag = format ? `@format ${escapeJsDoc(format)}` : undefined; if (descText && formatTag) { lines.push(`${indent} /**`); lines.push(`${indent} * ${descText}`); lines.push(`${indent} * ${formatTag}`); lines.push(`${indent} */`); } else { lines.push(`${indent} /** ${descText ?? formatTag} */`); } } const quotedName = quoteProp(propName); const optionalMark = isRequired ? "" : "?"; lines.push(`${indent} ${quotedName}${optionalMark}: ${propType};`); } // Handle additionalProperties if (schema.additionalProperties) { const valueType = schema.additionalProperties === true ? "unknown" : jsonSchemaToTypeString( schema.additionalProperties, indent + " ", nextCtx ); lines.push(`${indent} [key: string]: ${valueType};`); } if (lines.length === 0) { if (schema.additionalProperties === false) { return applyNullable("{}", schema); } return applyNullable("Record", schema); } const result = `{\n${lines.join("\n")}\n${indent}}`; return applyNullable(result, schema); } // Handle array of types (e.g., ["string", "null"]) if (Array.isArray(type)) { const types = type.map((t) => { if (t === "string") return "string"; if (t === "number" || t === "integer") return "number"; if (t === "boolean") return "boolean"; if (t === "null") return "null"; if (t === "array") return "unknown[]"; if (t === "object") return "Record"; return "unknown"; }); return applyNullable(types.join(" | "), schema); } return "unknown"; } finally { ctx.seen.delete(schema); } } /** * Convert a JSON Schema to a TypeScript type declaration. */ export function jsonSchemaToType( schema: JSONSchema7, typeName: string ): string { const ctx: ConversionContext = { root: schema, depth: 0, seen: new Set(), maxDepth: 20 }; const typeBody = jsonSchemaToTypeString(schema, "", ctx); return `type ${typeName} = ${typeBody}`; } /** * Extract field descriptions from a JSON Schema's properties. */ function extractJsonSchemaDescriptions( schema: JSONSchema7 ): Record { const descriptions: Record = {}; if (schema.properties) { for (const [fieldName, propSchema] of Object.entries(schema.properties)) { if ( propSchema && typeof propSchema === "object" && propSchema.description ) { descriptions[fieldName] = propSchema.description; } } } return descriptions; } /** * A tool descriptor using plain JSON Schema (no Zod or AI SDK dependency). */ export interface JsonSchemaToolDescriptor { description?: string; inputSchema: JSONSchema7; outputSchema?: JSONSchema7; } export type JsonSchemaToolDescriptors = Record< string, JsonSchemaToolDescriptor >; /** * Generate TypeScript type definitions from tool descriptors with JSON Schema. * This function has NO dependency on the AI SDK or Zod — it works purely with * JSON Schema objects. * * Use this when you have raw JSON Schema (e.g. from OpenAPI specs, MCP tool * definitions, etc.) and don't need the AI SDK. */ export function generateTypesFromJsonSchema( tools: JsonSchemaToolDescriptors ): string { let availableTools = ""; let availableTypes = ""; for (const [toolName, tool] of Object.entries(tools)) { const safeName = sanitizeToolName(toolName); const typeName = toPascalCase(safeName); try { const inputType = jsonSchemaToType(tool.inputSchema, `${typeName}Input`); const outputType = tool.outputSchema ? jsonSchemaToType(tool.outputSchema, `${typeName}Output`) : `type ${typeName}Output = unknown`; availableTypes += `\n${inputType.trim()}`; availableTypes += `\n${outputType.trim()}`; const paramLines = (() => { try { const paramDescs = extractJsonSchemaDescriptions(tool.inputSchema); return Object.entries(paramDescs).map( ([fieldName, desc]) => `@param input.${fieldName} - ${desc}` ); } catch { return []; } })(); const jsdocLines: string[] = []; if (tool.description?.trim()) { jsdocLines.push( escapeJsDoc(tool.description.trim().replace(/\r?\n/g, " ")) ); } else { jsdocLines.push(escapeJsDoc(toolName)); } for (const pd of paramLines) { jsdocLines.push(escapeJsDoc(pd.replace(/\r?\n/g, " "))); } const jsdocBody = jsdocLines.map((l) => `\t * ${l}`).join("\n"); availableTools += `\n\t/**\n${jsdocBody}\n\t */`; availableTools += `\n\t${safeName}: (input: ${typeName}Input) => Promise<${typeName}Output>;`; availableTools += "\n"; } catch { availableTypes += `\ntype ${typeName}Input = unknown`; availableTypes += `\ntype ${typeName}Output = unknown`; availableTools += `\n\t/**\n\t * ${escapeJsDoc(toolName)}\n\t */`; availableTools += `\n\t${safeName}: (input: ${typeName}Input) => Promise<${typeName}Output>;`; availableTools += "\n"; } } availableTools = `\ndeclare const codemode: {${availableTools}}`; return ` ${availableTypes} ${availableTools} `.trim(); }