diff --git a/src/SchemaGenerator.ts b/src/SchemaGenerator.ts index 4d9665f1d..fd8abb3b4 100644 --- a/src/SchemaGenerator.ts +++ b/src/SchemaGenerator.ts @@ -11,6 +11,7 @@ import type { StringMap } from "./Utils/StringMap.js"; import { hasJsDocTag } from "./Utils/hasJsDocTag.js"; import { removeUnreachable } from "./Utils/removeUnreachable.js"; import { symbolAtNode } from "./Utils/symbolAtNode.js"; +import { inlineSingleUse } from "./Utils/inlineSingleUse.js"; export class SchemaGenerator { public constructor( @@ -52,11 +53,13 @@ export class SchemaGenerator { {}, ); + const finalDefinitions = inlineSingleUse(rootTypeDefinition, reachableDefinitions); + return { ...(this.config?.schemaId ? { $id: this.config.schemaId } : {}), $schema: "http://json-schema.org/draft-07/schema#", ...(rootTypeDefinition ?? {}), - definitions: reachableDefinitions, + definitions: finalDefinitions, }; } diff --git a/src/Utils/inlineSingleUse.ts b/src/Utils/inlineSingleUse.ts new file mode 100644 index 000000000..d740b4273 --- /dev/null +++ b/src/Utils/inlineSingleUse.ts @@ -0,0 +1,95 @@ +import type { Definition } from "../Schema/Definition.js"; +import type { StringMap } from "./StringMap.js"; + +const DEF_PREFIX = "#/definitions/"; +const DEF_PREFIX_LEN = DEF_PREFIX.length; + +function countRefs(obj: any, counts: Map): void { + if (!obj || typeof obj !== "object") { + return; + } + if (Array.isArray(obj)) { + for (const item of obj) { + countRefs(item, counts); + } + return; + } + if (typeof obj.$ref === "string" && obj.$ref.startsWith(DEF_PREFIX)) { + const name = decodeURIComponent(obj.$ref.slice(DEF_PREFIX_LEN)); + counts.set(name, (counts.get(name) || 0) + 1); + } + for (const key in obj) { + if (key === "$ref") continue; + countRefs((obj as any)[key], counts); + } +} + +function replaceRef(obj: any, name: string, replacement: Definition): boolean { + if (!obj || typeof obj !== "object") { + return false; + } + if (Array.isArray(obj)) { + for (const item of obj) { + if (replaceRef(item, name, replacement)) { + return true; + } + } + return false; + } + if (typeof obj.$ref === "string" && obj.$ref.startsWith(DEF_PREFIX)) { + const refName = decodeURIComponent(obj.$ref.slice(DEF_PREFIX_LEN)); + if (refName === name) { + delete obj.$ref; + Object.assign(obj, replacement); + return true; + } + } + for (const key in obj) { + if (replaceRef((obj as any)[key], name, replacement)) { + return true; + } + } + return false; +} + +function isAutoGenerated(name: string): boolean { + return /(?:^|[<,])(def-)?(?:alias|object|structure)-\d/.test(name); +} + +export function inlineSingleUse( + root: Definition | undefined, + definitions: StringMap, +): StringMap { + const counts = new Map(); + if (root) { + countRefs(root, counts); + } + for (const def of Object.values(definitions)) { + countRefs(def, counts); + } + + const out: StringMap = { ...definitions }; + let changed = true; + while (changed) { + changed = false; + for (const [name, def] of Object.entries(out)) { + if (counts.get(name) === 1 && isAutoGenerated(name)) { + if (root && replaceRef(root, name, def)) { + delete out[name]; + changed = true; + break; + } + for (const [otherName, otherDef] of Object.entries(out)) { + if (otherName === name) continue; + if (replaceRef(otherDef, name, def)) { + delete out[name]; + changed = true; + break; + } + } + if (changed) break; + } + } + } + return out; +}