UNPKG

openapi-ts-json-schema

Version:

Generate TypeScript-first JSON schemas from OpenAPI definitions

256 lines (255 loc) 11.2 kB
import { existsSync } from 'fs'; import path from 'node:path'; import { $RefParser } from '@apidevtools/json-schema-ref-parser'; import get from 'lodash.get'; import { SCHEMA_ID_SYMBOL, addSchemaToMetaData, clearFolder, convertOpenApiDocumentDefinitionsToJsonSchema, convertOpenApiPathsParameters, formatTypeScript, makeId, makeRelativeImportPath, makeSchemaFileContents, parseSingleItemPath, patchJsonSchemaDefinitions, refToId, saveFile, saveSchemaFiles, } from './utils/index.js'; /** * @package openapi‑ts-json-schema * * Convert OpenAPI definitions to TypeScript JSON Schema modules. * * @remarks * This function resolves and dereferences `$ref`s in an OpenAPI document, converts OpenAPI schema objects * to JSON Schema, and writes out `.ts` modules exporting `as const` JSON schemas. It mirrors your OpenAPI * structure in the output and supports plugins (e.g. Fastify integration). * * @param options - Configuration for the conversion * @param options.openApiDocument - Path to an OpenAPI document (JSON or YAML) * @param options.targets - OpenAPI definition paths to generate JSON Schemas from * @param options.targets.collections - Array of paths pointing to objects/records of definitions, where each entry will be generated (eg: `["components.schemas"]`) * @param options.targets.single - Array of paths pointing to individual definitions to generate (eg: `["paths./users/{id}"]`) * @param options.refHandling - Strategy for `$ref` processing (`"import"` | `"inline"` | `"keep"`) (default: `"import"`) * @param options.moduleSystem - Controls how import specifiers are written in generated artifacts. Configure this option based on whether the consuming project is using CommonJS or ECMAScript modules * @param options.outputPath - Directory where generated schemas will be written. (default: `schemas-autogenerated`) * @param options.silent - If `true`, suppress logging output (default: `false`) * * @param options.idMapper - Optional function to map internal schema id strings to custom `$id` or import names * @param options.schemaPatcher - Hook called for every generated schema node, allowing programmatic mutation before output * @param options.plugins - Optional list of plugins for custom generation behavior * * @returns A Promise resolving to an object with: * - `outputPath`: the folder path where schemas were generated * - `metaData`: a map of schema identities to metadata (ids, file paths, import paths, etc.) * * @example * ```ts * import { openapiToTsJsonSchema } from "openapi‑ts-json-schema"; * * await openapiToTsJsonSchema({ * openApiDocument: "path/to/spec.yaml", * targets: { * collections: ["paths"], * }, * }); * ``` */ export async function openapiToTsJsonSchema(options) { const optionsWithDefaults = { refHandling: 'import', moduleSystem: 'esm', idMapper: ({ id }) => id, plugins: [], ...options, targets: { collections: options.targets.collections ?? [], single: options.targets.single ?? [], }, }; const { plugins } = optionsWithDefaults; // Execute plugins onInit method for (const { onInit } of plugins) { if (onInit) { await onInit({ options: optionsWithDefaults, }); } } const { openApiDocument: openApiDocumentRelative, schemaPatcher, outputPath: providedOutputPath, silent, refHandling, moduleSystem, idMapper, targets, } = optionsWithDefaults; if (targets.collections.length === 0 && targets.single.length === 0 && !silent) { console.log(`[openapi-ts-json-schema] ⚠️ No schemas will be generated since targets option is empty`); } [...targets.collections, ...targets.single].forEach((targetPath) => { if (path.isAbsolute(targetPath)) { throw new Error(`[openapi-ts-json-schema] "targets" must define relative paths. "${targetPath}" found.`); } }); const openApiDocumentPath = path.resolve(openApiDocumentRelative); if (!existsSync(openApiDocumentPath)) { throw new Error(`[openapi-ts-json-schema] Provided OpenAPI definition path doesn't exist: ${openApiDocumentPath}`); } const outputPath = providedOutputPath ?? path.resolve(path.dirname(openApiDocumentPath), 'schemas-autogenerated'); await clearFolder(outputPath); const openApiParser = new $RefParser(); const jsonSchemaParser = new $RefParser(); // Resolve and inline external $ref definitions // @ts-expect-error @apidevtools/json-schema-ref-parser is meant for JSON schemas but here we use it with OAS documents const bundledOpenApiDocument = await openApiParser.bundle(openApiDocumentPath); // Convert oas definitions to JSON schema (excluding paths and parameter objects) const openApiDocumentWithJsonSchemaDefinitions = convertOpenApiDocumentDefinitionsToJsonSchema(bundledOpenApiDocument); const patchedOpenApiDocument = patchJsonSchemaDefinitions(openApiDocumentWithJsonSchemaDefinitions, schemaPatcher); const inlinedRefs = new Map(); // Inline and collect internal $ref definitions // @ts-expect-error @apidevtools/json-schema-ref-parser is meant for JSON schemas but here we use it with OAS documents const dereferencedOpenApiDocument = await jsonSchemaParser.dereference(patchedOpenApiDocument, { dereference: { // @ts-expect-error onDereference seems not to be properly typed onDereference: (ref, inlinedSchema) => { const id = refToId(ref); // Keep track of inlined refs if (!inlinedRefs.has(id)) { // Shallow copy the ref schema to avoid the mutations below inlinedRefs.set(id, { // @ts-expect-error Spread types may only be created from object types openApiDefinition: openApiParser.$refs.get(ref), jsonSchema: { // @ts-expect-error Spread types may only be created from object types ...jsonSchemaParser.$refs.get(ref), }, }); } /** * mark inlined ref objects with a "SCHEMA_ID_SYMBOL" * to retrieve their id once inlined */ inlinedSchema[SCHEMA_ID_SYMBOL] = id; /** * "inline" refHandling support: * add a $ref comment to each inlined schema with the original ref value. * See: https://github.com/kaelzhang/node-comment-json */ if (refHandling === 'inline') { inlinedSchema[Symbol.for('before')] = [ { type: 'LineComment', value: ` $ref: "${ref}"`, }, ]; } }, }, }); const jsonSchema = convertOpenApiPathsParameters(dereferencedOpenApiDocument); const schemaMetaDataMap = new Map(); /** * Create meta data for each target */ for (const path of targets.single) { const jsonSchemaDefinition = get(jsonSchema, path); const openApiDefinition = get(bundledOpenApiDocument, path); if (!openApiDefinition) { throw new Error(`[openapi-ts-json-schema] target not found in OAS definition: "${path}"`); } // Handle single definition path const { schemaName, schemaRelativeDirName } = parseSingleItemPath(path); const id = makeId({ schemaRelativeDirName, schemaName, }); addSchemaToMetaData({ id, $id: idMapper({ id }), schemaMetaDataMap, openApiDefinition, jsonSchema: jsonSchemaDefinition, outputPath, isRef: inlinedRefs.has(id), shouldBeGenerated: true, }); } for (const path of targets.collections) { const jsonSchemaDefinitions = get(jsonSchema, path); const openApiDefinitions = get(bundledOpenApiDocument, path); if (!openApiDefinitions) { throw new Error(`[openapi-ts-json-schema] target not found in OAS definition: "${path}"`); } // Handle collection definition path for (const schemaName in jsonSchemaDefinitions) { const id = makeId({ schemaRelativeDirName: path, schemaName, }); addSchemaToMetaData({ id, $id: idMapper({ id }), schemaMetaDataMap, openApiDefinition: openApiDefinitions[schemaName], jsonSchema: jsonSchemaDefinitions[schemaName], outputPath, isRef: inlinedRefs.has(id), shouldBeGenerated: true, }); } } /** * Create meta data for each $ref schemas which have been previously dereferenced. */ for (const [id, { openApiDefinition, jsonSchema }] of inlinedRefs) { /** * In "inline" mode $ref schemas not explicitly marked for generation * should not be generated * * All the other "refHandling" modes generate all $ref schemas */ let shouldBeGenerated = true; if (refHandling === 'inline' && !schemaMetaDataMap.has(id)) { shouldBeGenerated = false; } addSchemaToMetaData({ id, $id: idMapper({ id }), schemaMetaDataMap, openApiDefinition, jsonSchema, outputPath, isRef: true, shouldBeGenerated, }); } const returnPayload = { outputPath, metaData: { schemas: schemaMetaDataMap }, }; // Execute plugins onBeforeGeneration method for (const { onBeforeGeneration } of plugins) { if (onBeforeGeneration) { await onBeforeGeneration({ ...returnPayload, options: optionsWithDefaults, utils: { makeRelativeImportPath, formatTypeScript, saveFile, }, }); } } // Generate schemas await makeSchemaFileContents({ refHandling, schemaMetaDataMap, idMapper, moduleSystem, }); // Execute plugins onBeforeGeneration method for (const { onBeforeSaveFile } of plugins) { if (onBeforeSaveFile) { await onBeforeSaveFile({ ...returnPayload, options: optionsWithDefaults, utils: { makeRelativeImportPath, formatTypeScript, saveFile, }, }); } } await saveSchemaFiles({ schemaMetaDataMap }); if (!silent) { console.log(`[openapi-ts-json-schema] ✅ JSON schema models generated at ${outputPath}`); } return returnPayload; }