diff --git a/packages/shared/src/config/parser/config.ts b/packages/shared/src/config/parser/config.ts index 27b07c951..37ef4db88 100644 --- a/packages/shared/src/config/parser/config.ts +++ b/packages/shared/src/config/parser/config.ts @@ -36,6 +36,10 @@ export function getParser(userConfig: { parser?: UserParser }): Parser { name: '{{name}}', }, }, + schemas: { + enabled: false, + name: '{{name}}', + }, }, validate_EXPERIMENTAL: false, }, @@ -137,6 +141,27 @@ export function getParser(userConfig: { parser?: UserParser }): Parser { }, value: fields.readWrite, }), + schemas: valueToObject({ + defaultValue: { + ...(defaultValue.schemas as Extract< + typeof defaultValue.schemas, + Record + >), + enabled: + fields.schemas !== undefined + ? Boolean(fields.schemas) + : ( + defaultValue.schemas as Extract< + typeof defaultValue.schemas, + Record + > + ).enabled, + }, + mappers: { + boolean: (enabled: boolean) => ({ enabled }), + }, + value: fields.schemas, + }), }), }, value: fields.transforms, diff --git a/packages/shared/src/config/parser/types.ts b/packages/shared/src/config/parser/types.ts index ed7ea0b13..28fb7557d 100644 --- a/packages/shared/src/config/parser/types.ts +++ b/packages/shared/src/config/parser/types.ts @@ -175,6 +175,48 @@ export type UserParser = { name?: NameTransformer; }; }; + /** + * Rename schema component keys and automatically update all `$ref` pointers + * throughout the specification. + * + * This is useful for: + * - Stripping version markers from schema names + * - Removing vendor prefixes + * - Converting naming conventions + * - Shortening verbose auto-generated names + * + * @example + * ```ts + * { + * schemas: { + * name: (name) => name.replace(/_v\d+_\d+_\d+_/, '_') + * } + * } + * ``` + * + * @default false + */ + schemas?: + | boolean + | { + /** + * Whether this feature is enabled. + * + * @default false + */ + enabled?: boolean; + /** + * Customize the generated name of schema components. + * When provided, this transformer is called for each schema key + * in `components.schemas` to compute the new name. + * + * If the new name conflicts with an existing schema, the rename + * is skipped for that schema. + * + * @default undefined + */ + name?: NameTransformer; + }; }; /** * **This is an experimental feature.** @@ -280,6 +322,23 @@ export type Parser = { */ responses: NamingOptions; }; + /** + * Rename schema component keys and automatically update all `$ref` pointers + * throughout the specification. + */ + schemas: FeatureToggle & { + /** + * Customize the generated name of schema components. + * When provided, this transformer is called for each schema key + * in `components.schemas` to compute the new name. + * + * If the new name conflicts with an existing schema, the rename + * is skipped for that schema. + * + * @default '{{name}}' + */ + name: NameTransformer; + }; }; /** * **This is an experimental feature.** diff --git a/packages/shared/src/openApi/shared/transforms/index.ts b/packages/shared/src/openApi/shared/transforms/index.ts index 534fa8a97..b43440558 100644 --- a/packages/shared/src/openApi/shared/transforms/index.ts +++ b/packages/shared/src/openApi/shared/transforms/index.ts @@ -2,10 +2,19 @@ import type { Context } from '../../../ir/context'; import { enumsTransform } from './enums'; import { propertiesRequiredByDefaultTransform } from './propertiesRequiredByDefault'; import { readWriteTransform } from './readWrite'; +import { schemasTransform } from './schemas'; export const transformOpenApiSpec = ({ context }: { context: Context }) => { const { logger } = context; const eventTransformOpenApiSpec = logger.timeEvent('transform-openapi-spec'); + + if (context.config.parser.transforms.schemas.enabled) { + schemasTransform({ + config: context.config.parser.transforms.schemas, + spec: context.spec, + }); + } + if (context.config.parser.transforms.enums.enabled) { enumsTransform({ config: context.config.parser.transforms.enums, diff --git a/packages/shared/src/openApi/shared/transforms/schemas.ts b/packages/shared/src/openApi/shared/transforms/schemas.ts new file mode 100644 index 000000000..272bf2c54 --- /dev/null +++ b/packages/shared/src/openApi/shared/transforms/schemas.ts @@ -0,0 +1,116 @@ +import type { Parser } from '../../../config/parser/types'; +import { applyNaming } from '../../../utils/naming/naming'; +import { getSchemasObject } from '../utils/transforms'; +import { specToSchemasPointerNamespace } from './utils'; + +type SchemasConfig = Parser['transforms']['schemas']; + +/** + * Recursively walks the entire spec object and replaces all $ref strings + * according to the provided rename mapping. + * + * @param node - Current node being visited + * @param renameMap - Map from old pointer to new pointer + */ +const rewriteRefs = (node: unknown, renameMap: Record) => { + if (node instanceof Array) { + node.forEach((item) => rewriteRefs(item, renameMap)); + } else if (node && typeof node === 'object') { + for (const [key, value] of Object.entries(node)) { + if (key === '$ref' && typeof value === 'string' && value in renameMap) { + // Replace the $ref with the new name + (node as Record)[key] = renameMap[value]; + } else { + rewriteRefs(value, renameMap); + } + } + } +}; + +/** + * Applies the schema name transform to rename schema component keys and + * update all $ref pointers throughout the spec. + * + * This transform: + * 1. Iterates all schema keys in components.schemas (or definitions for Swagger 2.0) + * 2. Applies the name transformer to compute new names + * 3. Handles name collisions (skips rename if new name already exists) + * 4. Renames schema keys in the schemas object + * 5. Updates all $ref pointers throughout the spec to use the new names + * + * @param config - The schemas transform config + * @param spec - The OpenAPI spec object to transform + */ +export const schemasTransform = ({ config, spec }: { config: SchemasConfig; spec: unknown }) => { + const schemasObj = getSchemasObject(spec); + if (!schemasObj) { + return; + } + + const schemasPointerNamespace = specToSchemasPointerNamespace(spec); + if (!schemasPointerNamespace) { + return; + } + + // Build rename map: oldPointer -> newPointer + const renameMap: Record = {}; + const newNames = new Set(); + + // First pass: compute all new names and check for collisions + for (const oldName of Object.keys(schemasObj)) { + const newName = applyNaming(oldName, config); + + // Skip if name doesn't change + if (newName === oldName) { + newNames.add(oldName); + continue; + } + + // Skip if new name collides with an existing schema or another renamed schema + if (oldName in schemasObj && newName in schemasObj && oldName !== newName) { + // Collision with existing schema - skip rename + newNames.add(oldName); + continue; + } + + if (newNames.has(newName)) { + // Collision with another renamed schema - skip rename + newNames.add(oldName); + continue; + } + + // Record the rename + renameMap[`${schemasPointerNamespace}${oldName}`] = `${schemasPointerNamespace}${newName}`; + newNames.add(newName); + } + + // Second pass: rename schema keys + // We need to be careful about the order to avoid overwriting + const renamedSchemas: Record = {}; + const processedOldNames = new Set(); + + for (const [oldPointer, newPointer] of Object.entries(renameMap)) { + const oldName = oldPointer.slice(schemasPointerNamespace.length); + const newName = newPointer.slice(schemasPointerNamespace.length); + + // Store the schema under the new name + renamedSchemas[newName] = schemasObj[oldName]; + processedOldNames.add(oldName); + } + + // Add all schemas that weren't renamed + for (const [name, schema] of Object.entries(schemasObj)) { + if (!processedOldNames.has(name)) { + renamedSchemas[name] = schema; + } + } + + // Replace the entire schemas object with the renamed version + Object.keys(schemasObj).forEach((key) => delete schemasObj[key]); + Object.assign(schemasObj, renamedSchemas); + + // Third pass: rewrite all $ref pointers throughout the spec + if (Object.keys(renameMap).length > 0) { + rewriteRefs(spec, renameMap); + } +}; -- 2.51.2 From b198d202a32cc8ca722d007b400041d056a3f3ce Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 22:13:43 +0000 Subject: [PATCH 03/14] test: add schema name transform test spec and integration test Co-authored-by: mrlubos <12529395+mrlubos@users.noreply.github.com> --- .github/workflows/pullfrog.yml | 1 - .../openapi-ts-tests/main/test/3.0.x.test.ts | 24 +++++++ specs/3.0.x/transforms-schemas-name.yaml | 70 +++++++++++++++++++ 3 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 specs/3.0.x/transforms-schemas-name.yaml diff --git a/.github/workflows/pullfrog.yml b/.github/workflows/pullfrog.yml index d86479432..e5a760711 100644 --- a/.github/workflows/pullfrog.yml +++ b/.github/workflows/pullfrog.yml @@ -43,4 +43,3 @@ jobs: GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }} DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }} OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} - diff --git a/packages/openapi-ts-tests/main/test/3.0.x.test.ts b/packages/openapi-ts-tests/main/test/3.0.x.test.ts index 77ed4def1..97bcca952 100644 --- a/packages/openapi-ts-tests/main/test/3.0.x.test.ts +++ b/packages/openapi-ts-tests/main/test/3.0.x.test.ts @@ -590,6 +590,30 @@ describe(`OpenAPI ${version}`, () => { }), description: 'handles read-only and write-only types', }, + { + config: createConfig({ + input: 'transforms-schemas-name.yaml', + output: 'transforms-schemas-name', + parser: { + transforms: { + schemas: { + name: (name: string) => { + // Strip version markers: User_v1_0_0_User → User + let clean = name.replace(/([A-Za-z\d]+)_v\d+_\d+_\d+_([A-Za-z\d]*)/g, (_, p1, p2) => + p2.startsWith(p1) ? p2 : p1 + p2, + ); + // Deduplicate prefixes: Foo_Foo → Foo + const m = clean.match(/^([A-Za-z\d]+)_\1([A-Za-z\d]*)$/); + if (m) clean = m[1] + m[2]; + return clean; + }, + }, + }, + }, + plugins: ['@hey-api/typescript'], + }), + description: 'handles schema name transforms', + }, { config: createConfig({ input: 'security-api-key.yaml', diff --git a/specs/3.0.x/transforms-schemas-name.yaml b/specs/3.0.x/transforms-schemas-name.yaml new file mode 100644 index 000000000..45aa8fa1a --- /dev/null +++ b/specs/3.0.x/transforms-schemas-name.yaml @@ -0,0 +1,70 @@ +openapi: 3.0.3 +info: + title: Schema Name Transform Test + version: 1.0.0 +paths: + /users: + get: + summary: Get users + responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/User_v1_0_0_User' + /posts: + post: + summary: Create post + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Post_v2_1_3_Post' + responses: + '201': + description: Created + content: + application/json: + schema: + $ref: '#/components/schemas/Post_v2_1_3_Post' +components: + schemas: + User_v1_0_0_User: + type: object + properties: + id: + type: string + name: + type: string + profile: + $ref: '#/components/schemas/UserProfile_v1_0_0_UserProfile' + UserProfile_v1_0_0_UserProfile: + type: object + properties: + bio: + type: string + avatar: + type: string + Post_v2_1_3_Post: + type: object + properties: + id: + type: string + title: + type: string + author: + $ref: '#/components/schemas/User_v1_0_0_User' + comments: + type: array + items: + $ref: '#/components/schemas/Comment_v1_5_2_Comment' + Comment_v1_5_2_Comment: + type: object + properties: + id: + type: string + text: + type: string + author: + $ref: '#/components/schemas/User_v1_0_0_User' -- 2.51.2 From b0fc6d21c8e42998c4393a9d22c0a14f8de646e3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 22:17:56 +0000 Subject: [PATCH 04/14] test: add Swagger 2.0 test for schema name transform Co-authored-by: mrlubos <12529395+mrlubos@users.noreply.github.com> --- .../openapi-ts-tests/main/test/2.0.x.test.ts | 24 +++++++ specs/2.0.x/transforms-schemas-name.yaml | 71 +++++++++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 specs/2.0.x/transforms-schemas-name.yaml diff --git a/packages/openapi-ts-tests/main/test/2.0.x.test.ts b/packages/openapi-ts-tests/main/test/2.0.x.test.ts index 672641f6f..00e3a1d08 100644 --- a/packages/openapi-ts-tests/main/test/2.0.x.test.ts +++ b/packages/openapi-ts-tests/main/test/2.0.x.test.ts @@ -287,6 +287,30 @@ describe(`OpenAPI ${version}`, () => { }), description: 'handles read-only and write-only types', }, + { + config: createConfig({ + input: 'transforms-schemas-name.yaml', + output: 'transforms-schemas-name', + parser: { + transforms: { + schemas: { + name: (name: string) => { + // Strip version markers: User_v1_0_0_User → User + let clean = name.replace(/([A-Za-z\d]+)_v\d+_\d+_\d+_([A-Za-z\d]*)/g, (_, p1, p2) => + p2.startsWith(p1) ? p2 : p1 + p2, + ); + // Deduplicate prefixes: Foo_Foo → Foo + const m = clean.match(/^([A-Za-z\d]+)_\1([A-Za-z\d]*)$/); + if (m) clean = m[1] + m[2]; + return clean; + }, + }, + }, + }, + plugins: ['@hey-api/typescript'], + }), + description: 'handles schema name transforms', + }, { config: createConfig({ input: 'schema-unknown.yaml', diff --git a/specs/2.0.x/transforms-schemas-name.yaml b/specs/2.0.x/transforms-schemas-name.yaml new file mode 100644 index 000000000..3383e4f8a --- /dev/null +++ b/specs/2.0.x/transforms-schemas-name.yaml @@ -0,0 +1,71 @@ +swagger: '2.0' +info: + title: Schema Name Transform Test (Swagger 2.0) + version: 1.0.0 +paths: + /users: + get: + summary: Get users + produces: + - application/json + responses: + '200': + description: Success + schema: + $ref: '#/definitions/User_v1_0_0_User' + /posts: + post: + summary: Create post + consumes: + - application/json + produces: + - application/json + parameters: + - in: body + name: body + schema: + $ref: '#/definitions/Post_v2_1_3_Post' + responses: + '201': + description: Created + schema: + $ref: '#/definitions/Post_v2_1_3_Post' +definitions: + User_v1_0_0_User: + type: object + properties: + id: + type: string + name: + type: string + profile: + $ref: '#/definitions/UserProfile_v1_0_0_UserProfile' + UserProfile_v1_0_0_UserProfile: + type: object + properties: + bio: + type: string + avatar: + type: string + Post_v2_1_3_Post: + type: object + properties: + id: + type: string + title: + type: string + author: + $ref: '#/definitions/User_v1_0_0_User' + comments: + type: array + items: + $ref: '#/definitions/Comment_v1_5_2_Comment' + Comment_v1_5_2_Comment: + type: object + properties: + id: + type: string + text: + type: string + author: + $ref: '#/definitions/User_v1_0_0_User' -- 2.51.2 From 8b454d6a87fd2336af61ef7ce4a52de505718868 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 22:19:24 +0000 Subject: [PATCH 05/14] docs: add parser.transforms.schemas configuration documentation Co-authored-by: mrlubos <12529395+mrlubos@users.noreply.github.com> --- docs/openapi-ts/configuration/parser.md | 68 +++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/docs/openapi-ts/configuration/parser.md b/docs/openapi-ts/configuration/parser.md index 9356206a6..1ceb6f4eb 100644 --- a/docs/openapi-ts/configuration/parser.md +++ b/docs/openapi-ts/configuration/parser.md @@ -447,6 +447,74 @@ export default { You can customize the naming and casing pattern for `requests` and `responses` schemas using the `.name` and `.case` options. +### Schemas + +Sometimes your schema names are auto-generated or follow a naming convention that produces verbose or awkward type names. The `schemas` transform allows you to rename schema component keys throughout the specification, automatically updating all `$ref` pointers. + +This is useful for: + +- Stripping version markers from schema names +- Removing vendor prefixes +- Converting naming conventions +- Shortening deeply qualified names + +::: code-group + +```js [function] +export default { + input: 'hey-api/backend', // sign up at app.heyapi.dev + output: 'src/client', + parser: { + transforms: { + schemas: { + name: (name) => { + // Strip version markers: ServiceRoot_v1_20_0_ServiceRoot → ServiceRoot + let clean = name.replace(/([A-Za-z\d]+)_v\d+_\d+_\d+_([A-Za-z\d]*)/g, (_, p1, p2) => + p2.startsWith(p1) ? p2 : p1 + p2, + ); + // Deduplicate prefixes: Foo_Foo → Foo + const m = clean.match(/^([A-Za-z\d]+)_\1([A-Za-z\d]*)$/); + if (m) clean = m[1] + m[2]; + return clean; + }, + }, + }, + }, +}; +``` + +```js [template] +export default { + input: 'hey-api/backend', // sign up at app.heyapi.dev + output: 'src/client', + parser: { + transforms: { + schemas: { + name: 'Api{{name}}', // Add "Api" prefix to all schemas + }, + }, + }, +}; +``` + +```js [disabled] +export default { + input: 'hey-api/backend', // sign up at app.heyapi.dev + output: 'src/client', + parser: { + transforms: { + schemas: false, // [!code ++] + }, + }, +}; +``` + +::: + +::: tip Name Collisions +If a transformed schema name conflicts with an existing schema, the rename is skipped for that schema to prevent overwrites. The original name is preserved. +::: + ## Pagination Paginated operations are detected by having a pagination keyword in its parameters or request body. By default, we consider the following to be pagination keywords: `after`, `before`, `cursor`, `offset`, `page`, and `start`. -- 2.51.2 From f9544fb8b18b87471b2555312c0ef8cf671b27c7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 22:48:36 +0000 Subject: [PATCH 06/14] refactor: flatten transforms.schemas.name to transforms.schemaName Co-authored-by: mrlubos <12529395+mrlubos@users.noreply.github.com> --- docs/openapi-ts/configuration/parser.md | 40 +++++------------ .../openapi-ts-tests/main/test/2.0.x.test.ts | 20 ++++----- .../openapi-ts-tests/main/test/3.0.x.test.ts | 20 ++++----- packages/shared/src/config/parser/config.ts | 28 ++---------- packages/shared/src/config/parser/types.ts | 44 +++---------------- .../src/openApi/shared/transforms/index.ts | 4 +- .../src/openApi/shared/transforms/schemas.ts | 22 ++++++++-- 7 files changed, 59 insertions(+), 119 deletions(-) diff --git a/docs/openapi-ts/configuration/parser.md b/docs/openapi-ts/configuration/parser.md index 1ceb6f4eb..7387a5835 100644 --- a/docs/openapi-ts/configuration/parser.md +++ b/docs/openapi-ts/configuration/parser.md @@ -447,9 +447,9 @@ export default { You can customize the naming and casing pattern for `requests` and `responses` schemas using the `.name` and `.case` options. -### Schemas +### Schema Name -Sometimes your schema names are auto-generated or follow a naming convention that produces verbose or awkward type names. The `schemas` transform allows you to rename schema component keys throughout the specification, automatically updating all `$ref` pointers. +Sometimes your schema names are auto-generated or follow a naming convention that produces verbose or awkward type names. The `schemaName` transform allows you to rename schema component keys throughout the specification, automatically updating all `$ref` pointers. This is useful for: @@ -466,17 +466,15 @@ export default { output: 'src/client', parser: { transforms: { - schemas: { - name: (name) => { - // Strip version markers: ServiceRoot_v1_20_0_ServiceRoot → ServiceRoot - let clean = name.replace(/([A-Za-z\d]+)_v\d+_\d+_\d+_([A-Za-z\d]*)/g, (_, p1, p2) => - p2.startsWith(p1) ? p2 : p1 + p2, - ); - // Deduplicate prefixes: Foo_Foo → Foo - const m = clean.match(/^([A-Za-z\d]+)_\1([A-Za-z\d]*)$/); - if (m) clean = m[1] + m[2]; - return clean; - }, + schemaName: (name) => { + // Strip version markers: ServiceRoot_v1_20_0_ServiceRoot → ServiceRoot + let clean = name.replace(/([A-Za-z\d]+)_v\d+_\d+_\d+_([A-Za-z\d]*)/g, (_, p1, p2) => + p2.startsWith(p1) ? p2 : p1 + p2, + ); + // Deduplicate prefixes: Foo_Foo → Foo + const m = clean.match(/^([A-Za-z\d]+)_\1([A-Za-z\d]*)$/); + if (m) clean = m[1] + m[2]; + return clean; }, }, }, @@ -489,21 +487,7 @@ export default { output: 'src/client', parser: { transforms: { - schemas: { - name: 'Api{{name}}', // Add "Api" prefix to all schemas - }, - }, - }, -}; -``` - -```js [disabled] -export default { - input: 'hey-api/backend', // sign up at app.heyapi.dev - output: 'src/client', - parser: { - transforms: { - schemas: false, // [!code ++] + schemaName: 'Api{{name}}', // Add "Api" prefix to all schemas }, }, }; diff --git a/packages/openapi-ts-tests/main/test/2.0.x.test.ts b/packages/openapi-ts-tests/main/test/2.0.x.test.ts index 00e3a1d08..9edb12831 100644 --- a/packages/openapi-ts-tests/main/test/2.0.x.test.ts +++ b/packages/openapi-ts-tests/main/test/2.0.x.test.ts @@ -293,17 +293,15 @@ describe(`OpenAPI ${version}`, () => { output: 'transforms-schemas-name', parser: { transforms: { - schemas: { - name: (name: string) => { - // Strip version markers: User_v1_0_0_User → User - let clean = name.replace(/([A-Za-z\d]+)_v\d+_\d+_\d+_([A-Za-z\d]*)/g, (_, p1, p2) => - p2.startsWith(p1) ? p2 : p1 + p2, - ); - // Deduplicate prefixes: Foo_Foo → Foo - const m = clean.match(/^([A-Za-z\d]+)_\1([A-Za-z\d]*)$/); - if (m) clean = m[1] + m[2]; - return clean; - }, + schemaName: (name: string) => { + // Strip version markers: User_v1_0_0_User → User + let clean = name.replace(/([A-Za-z\d]+)_v\d+_\d+_\d+_([A-Za-z\d]*)/g, (_, p1, p2) => + p2.startsWith(p1) ? p2 : p1 + p2, + ); + // Deduplicate prefixes: Foo_Foo → Foo + const m = clean.match(/^([A-Za-z\d]+)_\1([A-Za-z\d]*)$/); + if (m) clean = m[1] + m[2]; + return clean; }, }, }, diff --git a/packages/openapi-ts-tests/main/test/3.0.x.test.ts b/packages/openapi-ts-tests/main/test/3.0.x.test.ts index 97bcca952..c870ba89d 100644 --- a/packages/openapi-ts-tests/main/test/3.0.x.test.ts +++ b/packages/openapi-ts-tests/main/test/3.0.x.test.ts @@ -596,17 +596,15 @@ describe(`OpenAPI ${version}`, () => { output: 'transforms-schemas-name', parser: { transforms: { - schemas: { - name: (name: string) => { - // Strip version markers: User_v1_0_0_User → User - let clean = name.replace(/([A-Za-z\d]+)_v\d+_\d+_\d+_([A-Za-z\d]*)/g, (_, p1, p2) => - p2.startsWith(p1) ? p2 : p1 + p2, - ); - // Deduplicate prefixes: Foo_Foo → Foo - const m = clean.match(/^([A-Za-z\d]+)_\1([A-Za-z\d]*)$/); - if (m) clean = m[1] + m[2]; - return clean; - }, + schemaName: (name: string) => { + // Strip version markers: User_v1_0_0_User → User + let clean = name.replace(/([A-Za-z\d]+)_v\d+_\d+_\d+_([A-Za-z\d]*)/g, (_, p1, p2) => + p2.startsWith(p1) ? p2 : p1 + p2, + ); + // Deduplicate prefixes: Foo_Foo → Foo + const m = clean.match(/^([A-Za-z\d]+)_\1([A-Za-z\d]*)$/); + if (m) clean = m[1] + m[2]; + return clean; }, }, }, diff --git a/packages/shared/src/config/parser/config.ts b/packages/shared/src/config/parser/config.ts index 37ef4db88..a9c013800 100644 --- a/packages/shared/src/config/parser/config.ts +++ b/packages/shared/src/config/parser/config.ts @@ -36,10 +36,7 @@ export function getParser(userConfig: { parser?: UserParser }): Parser { name: '{{name}}', }, }, - schemas: { - enabled: false, - name: '{{name}}', - }, + schemaName: undefined, }, validate_EXPERIMENTAL: false, }, @@ -141,27 +138,8 @@ export function getParser(userConfig: { parser?: UserParser }): Parser { }, value: fields.readWrite, }), - schemas: valueToObject({ - defaultValue: { - ...(defaultValue.schemas as Extract< - typeof defaultValue.schemas, - Record - >), - enabled: - fields.schemas !== undefined - ? Boolean(fields.schemas) - : ( - defaultValue.schemas as Extract< - typeof defaultValue.schemas, - Record - > - ).enabled, - }, - mappers: { - boolean: (enabled: boolean) => ({ enabled }), - }, - value: fields.schemas, - }), + schemaName: + fields.schemaName !== undefined ? fields.schemaName : defaultValue.schemaName, }), }, value: fields.transforms, diff --git a/packages/shared/src/config/parser/types.ts b/packages/shared/src/config/parser/types.ts index 28fb7557d..a1f17a749 100644 --- a/packages/shared/src/config/parser/types.ts +++ b/packages/shared/src/config/parser/types.ts @@ -188,35 +188,13 @@ export type UserParser = { * @example * ```ts * { - * schemas: { - * name: (name) => name.replace(/_v\d+_\d+_\d+_/, '_') - * } + * schemaName: (name) => name.replace(/_v\d+_\d+_\d+_/, '_') * } * ``` * - * @default false + * @default undefined */ - schemas?: - | boolean - | { - /** - * Whether this feature is enabled. - * - * @default false - */ - enabled?: boolean; - /** - * Customize the generated name of schema components. - * When provided, this transformer is called for each schema key - * in `components.schemas` to compute the new name. - * - * If the new name conflicts with an existing schema, the rename - * is skipped for that schema. - * - * @default undefined - */ - name?: NameTransformer; - }; + schemaName?: NameTransformer; }; /** * **This is an experimental feature.** @@ -325,20 +303,10 @@ export type Parser = { /** * Rename schema component keys and automatically update all `$ref` pointers * throughout the specification. + * + * @default undefined */ - schemas: FeatureToggle & { - /** - * Customize the generated name of schema components. - * When provided, this transformer is called for each schema key - * in `components.schemas` to compute the new name. - * - * If the new name conflicts with an existing schema, the rename - * is skipped for that schema. - * - * @default '{{name}}' - */ - name: NameTransformer; - }; + schemaName?: NameTransformer; }; /** * **This is an experimental feature.** diff --git a/packages/shared/src/openApi/shared/transforms/index.ts b/packages/shared/src/openApi/shared/transforms/index.ts index b43440558..becd9a139 100644 --- a/packages/shared/src/openApi/shared/transforms/index.ts +++ b/packages/shared/src/openApi/shared/transforms/index.ts @@ -8,9 +8,9 @@ export const transformOpenApiSpec = ({ context }: { context: Context }) => { const { logger } = context; const eventTransformOpenApiSpec = logger.timeEvent('transform-openapi-spec'); - if (context.config.parser.transforms.schemas.enabled) { + if (context.config.parser.transforms.schemaName) { schemasTransform({ - config: context.config.parser.transforms.schemas, + schemaName: context.config.parser.transforms.schemaName, spec: context.spec, }); } diff --git a/packages/shared/src/openApi/shared/transforms/schemas.ts b/packages/shared/src/openApi/shared/transforms/schemas.ts index 272bf2c54..4efb66971 100644 --- a/packages/shared/src/openApi/shared/transforms/schemas.ts +++ b/packages/shared/src/openApi/shared/transforms/schemas.ts @@ -3,7 +3,7 @@ import { applyNaming } from '../../../utils/naming/naming'; import { getSchemasObject } from '../utils/transforms'; import { specToSchemasPointerNamespace } from './utils'; -type SchemasConfig = Parser['transforms']['schemas']; +type SchemaNameConfig = Parser['transforms']['schemaName']; /** * Recursively walks the entire spec object and replaces all $ref strings @@ -38,10 +38,20 @@ const rewriteRefs = (node: unknown, renameMap: Record) => { * 4. Renames schema keys in the schemas object * 5. Updates all $ref pointers throughout the spec to use the new names * - * @param config - The schemas transform config + * @param schemaName - The schema name transformer * @param spec - The OpenAPI spec object to transform */ -export const schemasTransform = ({ config, spec }: { config: SchemasConfig; spec: unknown }) => { +export const schemasTransform = ({ + schemaName, + spec, +}: { + schemaName: SchemaNameConfig; + spec: unknown; +}) => { + if (!schemaName) { + return; + } + const schemasObj = getSchemasObject(spec); if (!schemasObj) { return; @@ -56,9 +66,13 @@ export const schemasTransform = ({ config, spec }: { config: SchemasConfig; spec const renameMap: Record = {}; const newNames = new Set(); + // Create a simple config object for applyNaming + const namingConfig = + typeof schemaName === 'function' ? { name: schemaName } : { name: schemaName }; + // First pass: compute all new names and check for collisions for (const oldName of Object.keys(schemasObj)) { - const newName = applyNaming(oldName, config); + const newName = applyNaming(oldName, namingConfig); // Skip if name doesn't change if (newName === oldName) { -- 2.51.2 From ee162aee56ca83df5817c54af3656f5aa35840f5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 23:11:17 +0000 Subject: [PATCH 07/14] refactor: rename to schemaNameTransform, use config param, fix docs style Co-authored-by: mrlubos <12529395+mrlubos@users.noreply.github.com> --- docs/openapi-ts/configuration/parser.md | 35 +++++----- .../openapi-ts-tests/main/test/3.1.x.test.ts | 22 ++++++ packages/shared/src/config/parser/types.ts | 11 +-- .../src/openApi/shared/transforms/index.ts | 6 +- .../src/openApi/shared/transforms/schemas.ts | 13 ++-- specs/3.1.x/transforms-schemas-name.yaml | 70 +++++++++++++++++++ 6 files changed, 121 insertions(+), 36 deletions(-) create mode 100644 specs/3.1.x/transforms-schemas-name.yaml diff --git a/docs/openapi-ts/configuration/parser.md b/docs/openapi-ts/configuration/parser.md index 7387a5835..ec1778e2a 100644 --- a/docs/openapi-ts/configuration/parser.md +++ b/docs/openapi-ts/configuration/parser.md @@ -449,14 +449,7 @@ You can customize the naming and casing pattern for `requests` and `responses` s ### Schema Name -Sometimes your schema names are auto-generated or follow a naming convention that produces verbose or awkward type names. The `schemaName` transform allows you to rename schema component keys throughout the specification, automatically updating all `$ref` pointers. - -This is useful for: - -- Stripping version markers from schema names -- Removing vendor prefixes -- Converting naming conventions -- Shortening deeply qualified names +Sometimes your schema names are auto-generated or follow a naming convention that produces verbose or awkward type names. You can rename schema component keys throughout the specification, automatically updating all `$ref` pointers. For example, stripping version markers from schema names, removing vendor prefixes, converting naming conventions, or shortening deeply qualified names. ::: code-group @@ -467,15 +460,21 @@ export default { parser: { transforms: { schemaName: (name) => { - // Strip version markers: ServiceRoot_v1_20_0_ServiceRoot → ServiceRoot - let clean = name.replace(/([A-Za-z\d]+)_v\d+_\d+_\d+_([A-Za-z\d]*)/g, (_, p1, p2) => - p2.startsWith(p1) ? p2 : p1 + p2, - ); - // Deduplicate prefixes: Foo_Foo → Foo - const m = clean.match(/^([A-Za-z\d]+)_\1([A-Za-z\d]*)$/); - if (m) clean = m[1] + m[2]; - return clean; - }, + // [!code ++] + // Strip version markers: ServiceRoot_v1_20_0_ServiceRoot → ServiceRoot // [!code ++] + let clean = name.replace( + /([A-Za-z\d]+)_v\d+_\d+_\d+_([A-Za-z\d]*)/g, + ( + _, + p1, + p2, // [!code ++] + ) => (p2.startsWith(p1) ? p2 : p1 + p2), // [!code ++] + ); // [!code ++] + // Deduplicate prefixes: Foo_Foo → Foo // [!code ++] + const m = clean.match(/^([A-Za-z\d]+)_\1([A-Za-z\d]*)$/); // [!code ++] + if (m) clean = m[1] + m[2]; // [!code ++] + return clean; // [!code ++] + }, // [!code ++] }, }, }; @@ -487,7 +486,7 @@ export default { output: 'src/client', parser: { transforms: { - schemaName: 'Api{{name}}', // Add "Api" prefix to all schemas + schemaName: 'Api{{name}}', // [!code ++] }, }, }; diff --git a/packages/openapi-ts-tests/main/test/3.1.x.test.ts b/packages/openapi-ts-tests/main/test/3.1.x.test.ts index 1bee7b8cb..0ed636eae 100644 --- a/packages/openapi-ts-tests/main/test/3.1.x.test.ts +++ b/packages/openapi-ts-tests/main/test/3.1.x.test.ts @@ -692,6 +692,28 @@ describe(`OpenAPI ${version}`, () => { }), description: 'handles read-only and write-only types', }, + { + config: createConfig({ + input: 'transforms-schemas-name.yaml', + output: 'transforms-schemas-name', + parser: { + transforms: { + schemaName: (name: string) => { + // Strip version markers: User_v1_0_0_User → User + let clean = name.replace(/([A-Za-z\d]+)_v\d+_\d+_\d+_([A-Za-z\d]*)/g, (_, p1, p2) => + p2.startsWith(p1) ? p2 : p1 + p2, + ); + // Deduplicate prefixes: Foo_Foo → Foo + const m = clean.match(/^([A-Za-z\d]+)_\1([A-Za-z\d]*)$/); + if (m) clean = m[1] + m[2]; + return clean; + }, + }, + }, + plugins: ['@hey-api/typescript'], + }), + description: 'handles schema name transforms', + }, { config: createConfig({ input: 'transforms-read-write-nested.yaml', diff --git a/packages/shared/src/config/parser/types.ts b/packages/shared/src/config/parser/types.ts index a1f17a749..ce7910bf5 100644 --- a/packages/shared/src/config/parser/types.ts +++ b/packages/shared/src/config/parser/types.ts @@ -176,14 +176,9 @@ export type UserParser = { }; }; /** - * Rename schema component keys and automatically update all `$ref` pointers - * throughout the specification. - * - * This is useful for: - * - Stripping version markers from schema names - * - Removing vendor prefixes - * - Converting naming conventions - * - Shortening verbose auto-generated names + * Sometimes your schema names are auto-generated or follow a naming convention + * that produces verbose or awkward type names. You can rename schema component + * keys throughout the specification, automatically updating all `$ref` pointers. * * @example * ```ts diff --git a/packages/shared/src/openApi/shared/transforms/index.ts b/packages/shared/src/openApi/shared/transforms/index.ts index becd9a139..ec7840d84 100644 --- a/packages/shared/src/openApi/shared/transforms/index.ts +++ b/packages/shared/src/openApi/shared/transforms/index.ts @@ -2,15 +2,15 @@ import type { Context } from '../../../ir/context'; import { enumsTransform } from './enums'; import { propertiesRequiredByDefaultTransform } from './propertiesRequiredByDefault'; import { readWriteTransform } from './readWrite'; -import { schemasTransform } from './schemas'; +import { schemaNameTransform } from './schemas'; export const transformOpenApiSpec = ({ context }: { context: Context }) => { const { logger } = context; const eventTransformOpenApiSpec = logger.timeEvent('transform-openapi-spec'); if (context.config.parser.transforms.schemaName) { - schemasTransform({ - schemaName: context.config.parser.transforms.schemaName, + schemaNameTransform({ + config: context.config.parser.transforms.schemaName, spec: context.spec, }); } diff --git a/packages/shared/src/openApi/shared/transforms/schemas.ts b/packages/shared/src/openApi/shared/transforms/schemas.ts index 4efb66971..66d3dae4b 100644 --- a/packages/shared/src/openApi/shared/transforms/schemas.ts +++ b/packages/shared/src/openApi/shared/transforms/schemas.ts @@ -38,17 +38,17 @@ const rewriteRefs = (node: unknown, renameMap: Record) => { * 4. Renames schema keys in the schemas object * 5. Updates all $ref pointers throughout the spec to use the new names * - * @param schemaName - The schema name transformer + * @param config - The schema name transformer * @param spec - The OpenAPI spec object to transform */ -export const schemasTransform = ({ - schemaName, +export const schemaNameTransform = ({ + config, spec, }: { - schemaName: SchemaNameConfig; + config: SchemaNameConfig; spec: unknown; }) => { - if (!schemaName) { + if (!config) { return; } @@ -67,8 +67,7 @@ export const schemasTransform = ({ const newNames = new Set(); // Create a simple config object for applyNaming - const namingConfig = - typeof schemaName === 'function' ? { name: schemaName } : { name: schemaName }; + const namingConfig = typeof config === 'function' ? { name: config } : { name: config }; // First pass: compute all new names and check for collisions for (const oldName of Object.keys(schemasObj)) { diff --git a/specs/3.1.x/transforms-schemas-name.yaml b/specs/3.1.x/transforms-schemas-name.yaml new file mode 100644 index 000000000..e7696be80 --- /dev/null +++ b/specs/3.1.x/transforms-schemas-name.yaml @@ -0,0 +1,70 @@ +openapi: 3.1.0 +info: + title: Schema Name Transform Test + version: 1.0.0 +paths: + /users: + get: + summary: Get users + responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/User_v1_0_0_User' + /posts: + post: + summary: Create post + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Post_v2_1_3_Post' + responses: + '201': + description: Created + content: + application/json: + schema: + $ref: '#/components/schemas/Post_v2_1_3_Post' +components: + schemas: + User_v1_0_0_User: + type: object + properties: + id: + type: string + name: + type: string + profile: + $ref: '#/components/schemas/UserProfile_v1_0_0_UserProfile' + UserProfile_v1_0_0_UserProfile: + type: object + properties: + bio: + type: string + avatar: + type: string + Post_v2_1_3_Post: + type: object + properties: + id: + type: string + title: + type: string + author: + $ref: '#/components/schemas/User_v1_0_0_User' + comments: + type: array + items: + $ref: '#/components/schemas/Comment_v1_5_2_Comment' + Comment_v1_5_2_Comment: + type: object + properties: + id: + type: string + text: + type: string + author: + $ref: '#/components/schemas/User_v1_0_0_User' -- 2.51.2 From 0446de4b01d5308314b6524e3260afd9dd7d588f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 23:13:24 +0000 Subject: [PATCH 08/14] test: add snapshots for schema name transform tests Co-authored-by: mrlubos <12529395+mrlubos@users.noreply.github.com> --- .../2.0.x/transforms-schemas-name/index.ts | 3 + .../transforms-schemas-name/types.gen.ts | 61 +++++++++++++++++++ .../3.0.x/transforms-schemas-name/index.ts | 3 + .../transforms-schemas-name/types.gen.ts | 61 +++++++++++++++++++ .../3.1.x/transforms-schemas-name/index.ts | 3 + .../transforms-schemas-name/types.gen.ts | 61 +++++++++++++++++++ 6 files changed, 192 insertions(+) create mode 100644 packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/transforms-schemas-name/index.ts create mode 100644 packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/transforms-schemas-name/types.gen.ts create mode 100644 packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/transforms-schemas-name/index.ts create mode 100644 packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/transforms-schemas-name/types.gen.ts create mode 100644 packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/transforms-schemas-name/index.ts create mode 100644 packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/transforms-schemas-name/types.gen.ts diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/transforms-schemas-name/index.ts b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/transforms-schemas-name/index.ts new file mode 100644 index 000000000..d35066307 --- /dev/null +++ b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/transforms-schemas-name/index.ts @@ -0,0 +1,3 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type { ClientOptions, Comment, GetUsersData, GetUsersResponse, GetUsersResponses, Post, PostPostsData, PostPostsResponse, PostPostsResponses, User, UserProfile } from './types.gen'; diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/transforms-schemas-name/types.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/transforms-schemas-name/types.gen.ts new file mode 100644 index 000000000..fef4ad857 --- /dev/null +++ b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/transforms-schemas-name/types.gen.ts @@ -0,0 +1,61 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type ClientOptions = { + baseUrl: string; +}; + +export type User = { + id?: string; + name?: string; + profile?: UserProfile; +}; + +export type UserProfile = { + bio?: string; + avatar?: string; +}; + +export type Post = { + id?: string; + title?: string; + author?: User; + comments?: Array; +}; + +export type Comment = { + id?: string; + text?: string; + author?: User; +}; + +export type GetUsersData = { + body?: never; + path?: never; + query?: never; + url: '/users'; +}; + +export type GetUsersResponses = { + /** + * Success + */ + 200: User; +}; + +export type GetUsersResponse = GetUsersResponses[keyof GetUsersResponses]; + +export type PostPostsData = { + body?: Post; + path?: never; + query?: never; + url: '/posts'; +}; + +export type PostPostsResponses = { + /** + * Created + */ + 201: Post; +}; + +export type PostPostsResponse = PostPostsResponses[keyof PostPostsResponses]; diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/transforms-schemas-name/index.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/transforms-schemas-name/index.ts new file mode 100644 index 000000000..d35066307 --- /dev/null +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/transforms-schemas-name/index.ts @@ -0,0 +1,3 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type { ClientOptions, Comment, GetUsersData, GetUsersResponse, GetUsersResponses, Post, PostPostsData, PostPostsResponse, PostPostsResponses, User, UserProfile } from './types.gen'; diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/transforms-schemas-name/types.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/transforms-schemas-name/types.gen.ts new file mode 100644 index 000000000..5f9aa1bec --- /dev/null +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/transforms-schemas-name/types.gen.ts @@ -0,0 +1,61 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type ClientOptions = { + baseUrl: `${string}://${string}` | (string & {}); +}; + +export type User = { + id?: string; + name?: string; + profile?: UserProfile; +}; + +export type UserProfile = { + bio?: string; + avatar?: string; +}; + +export type Post = { + id?: string; + title?: string; + author?: User; + comments?: Array; +}; + +export type Comment = { + id?: string; + text?: string; + author?: User; +}; + +export type GetUsersData = { + body?: never; + path?: never; + query?: never; + url: '/users'; +}; + +export type GetUsersResponses = { + /** + * Success + */ + 200: User; +}; + +export type GetUsersResponse = GetUsersResponses[keyof GetUsersResponses]; + +export type PostPostsData = { + body?: Post; + path?: never; + query?: never; + url: '/posts'; +}; + +export type PostPostsResponses = { + /** + * Created + */ + 201: Post; +}; + +export type PostPostsResponse = PostPostsResponses[keyof PostPostsResponses]; diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/transforms-schemas-name/index.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/transforms-schemas-name/index.ts new file mode 100644 index 000000000..d35066307 --- /dev/null +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/transforms-schemas-name/index.ts @@ -0,0 +1,3 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type { ClientOptions, Comment, GetUsersData, GetUsersResponse, GetUsersResponses, Post, PostPostsData, PostPostsResponse, PostPostsResponses, User, UserProfile } from './types.gen'; diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/transforms-schemas-name/types.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/transforms-schemas-name/types.gen.ts new file mode 100644 index 000000000..5f9aa1bec --- /dev/null +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/transforms-schemas-name/types.gen.ts @@ -0,0 +1,61 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type ClientOptions = { + baseUrl: `${string}://${string}` | (string & {}); +}; + +export type User = { + id?: string; + name?: string; + profile?: UserProfile; +}; + +export type UserProfile = { + bio?: string; + avatar?: string; +}; + +export type Post = { + id?: string; + title?: string; + author?: User; + comments?: Array; +}; + +export type Comment = { + id?: string; + text?: string; + author?: User; +}; + +export type GetUsersData = { + body?: never; + path?: never; + query?: never; + url: '/users'; +}; + +export type GetUsersResponses = { + /** + * Success + */ + 200: User; +}; + +export type GetUsersResponse = GetUsersResponses[keyof GetUsersResponses]; + +export type PostPostsData = { + body?: Post; + path?: never; + query?: never; + url: '/posts'; +}; + +export type PostPostsResponses = { + /** + * Created + */ + 201: Post; +}; + +export type PostPostsResponse = PostPostsResponses[keyof PostPostsResponses]; -- 2.51.2 From abf8ad853992232af7a5442651981c56ef81d4f1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 23:57:21 +0000 Subject: [PATCH 09/14] fix: simplify schemaNameTransform logic and add prettier-ignore to docs Co-authored-by: mrlubos <12529395+mrlubos@users.noreply.github.com> --- docs/openapi-ts/configuration/parser.md | 14 +++--- .../openapi-ts-tests/main/test/3.0.x.test.ts | 17 +++++++ .../index.ts | 3 ++ .../types.gen.ts | 39 ++++++++++++++++ .../src/openApi/shared/transforms/schemas.ts | 10 ++--- .../transforms-schemas-name-collision.yaml | 44 +++++++++++++++++++ 6 files changed, 111 insertions(+), 16 deletions(-) create mode 100644 packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/transforms-schemas-name-collision/index.ts create mode 100644 packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/transforms-schemas-name-collision/types.gen.ts create mode 100644 specs/3.0.x/transforms-schemas-name-collision.yaml diff --git a/docs/openapi-ts/configuration/parser.md b/docs/openapi-ts/configuration/parser.md index ec1778e2a..d91a8a670 100644 --- a/docs/openapi-ts/configuration/parser.md +++ b/docs/openapi-ts/configuration/parser.md @@ -453,22 +453,17 @@ Sometimes your schema names are auto-generated or follow a naming convention tha ::: code-group + ```js [function] export default { input: 'hey-api/backend', // sign up at app.heyapi.dev output: 'src/client', parser: { transforms: { - schemaName: (name) => { - // [!code ++] + schemaName: (name) => { // [!code ++] // Strip version markers: ServiceRoot_v1_20_0_ServiceRoot → ServiceRoot // [!code ++] - let clean = name.replace( - /([A-Za-z\d]+)_v\d+_\d+_\d+_([A-Za-z\d]*)/g, - ( - _, - p1, - p2, // [!code ++] - ) => (p2.startsWith(p1) ? p2 : p1 + p2), // [!code ++] + let clean = name.replace(/([A-Za-z\d]+)_v\d+_\d+_\d+_([A-Za-z\d]*)/g, (_, p1, p2) => // [!code ++] + p2.startsWith(p1) ? p2 : p1 + p2, // [!code ++] ); // [!code ++] // Deduplicate prefixes: Foo_Foo → Foo // [!code ++] const m = clean.match(/^([A-Za-z\d]+)_\1([A-Za-z\d]*)$/); // [!code ++] @@ -479,6 +474,7 @@ export default { }, }; ``` + ```js [template] export default { diff --git a/packages/openapi-ts-tests/main/test/3.0.x.test.ts b/packages/openapi-ts-tests/main/test/3.0.x.test.ts index c870ba89d..7f2d5e89f 100644 --- a/packages/openapi-ts-tests/main/test/3.0.x.test.ts +++ b/packages/openapi-ts-tests/main/test/3.0.x.test.ts @@ -612,6 +612,23 @@ describe(`OpenAPI ${version}`, () => { }), description: 'handles schema name transforms', }, + { + config: createConfig({ + input: 'transforms-schemas-name-collision.yaml', + output: 'transforms-schemas-name-collision', + parser: { + transforms: { + schemaName: (name: string) => + // Try to rename all _vX_User schemas to "User" + // This should cause collisions since "User" already exists + name.replace(/_v\d+_User$/, '') + , + }, + }, + plugins: ['@hey-api/typescript'], + }), + description: 'handles schema name collision prevention', + }, { config: createConfig({ input: 'security-api-key.yaml', diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/transforms-schemas-name-collision/index.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/transforms-schemas-name-collision/index.ts new file mode 100644 index 000000000..80371cc73 --- /dev/null +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/transforms-schemas-name-collision/index.ts @@ -0,0 +1,3 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type { ClientOptions, GetTestData, GetTestResponse, GetTestResponses, User, UserV1User, UserV2User } from './types.gen'; diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/transforms-schemas-name-collision/types.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/transforms-schemas-name-collision/types.gen.ts new file mode 100644 index 000000000..2abb98d38 --- /dev/null +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/transforms-schemas-name-collision/types.gen.ts @@ -0,0 +1,39 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type ClientOptions = { + baseUrl: `${string}://${string}` | (string & {}); +}; + +export type UserV1User = { + id?: string; + version?: 'v1'; +}; + +export type User = { + name?: string; +}; + +export type UserV2User = { + email?: string; + version?: 'v2'; +}; + +export type GetTestData = { + body?: never; + path?: never; + query?: never; + url: '/test'; +}; + +export type GetTestResponses = { + /** + * Success + */ + 200: { + user1?: UserV1User; + user2?: User; + user3?: UserV2User; + }; +}; + +export type GetTestResponse = GetTestResponses[keyof GetTestResponses]; diff --git a/packages/shared/src/openApi/shared/transforms/schemas.ts b/packages/shared/src/openApi/shared/transforms/schemas.ts index 66d3dae4b..568bf3cc9 100644 --- a/packages/shared/src/openApi/shared/transforms/schemas.ts +++ b/packages/shared/src/openApi/shared/transforms/schemas.ts @@ -75,20 +75,16 @@ export const schemaNameTransform = ({ // Skip if name doesn't change if (newName === oldName) { - newNames.add(oldName); continue; } - // Skip if new name collides with an existing schema or another renamed schema - if (oldName in schemasObj && newName in schemasObj && oldName !== newName) { - // Collision with existing schema - skip rename - newNames.add(oldName); + // Skip if new name collides with an existing schema + if (newName in schemasObj) { continue; } + // Skip if new name collides with another renamed schema if (newNames.has(newName)) { - // Collision with another renamed schema - skip rename - newNames.add(oldName); continue; } diff --git a/specs/3.0.x/transforms-schemas-name-collision.yaml b/specs/3.0.x/transforms-schemas-name-collision.yaml new file mode 100644 index 000000000..46e308a78 --- /dev/null +++ b/specs/3.0.x/transforms-schemas-name-collision.yaml @@ -0,0 +1,44 @@ +openapi: 3.0.3 +info: + title: Schema Name Collision Test + version: 1.0.0 +paths: + /test: + get: + responses: + '200': + description: Success + content: + application/json: + schema: + type: object + properties: + user1: + $ref: '#/components/schemas/User_v1_User' + user2: + $ref: '#/components/schemas/User' + user3: + $ref: '#/components/schemas/User_v2_User' +components: + schemas: + User_v1_User: + type: object + properties: + id: + type: string + version: + type: string + enum: ['v1'] + User: + type: object + properties: + name: + type: string + User_v2_User: + type: object + properties: + email: + type: string + version: + type: string + enum: ['v2'] -- 2.51.2 From 2e40124cf99c778e98e1dd6f5e7be6e2224853c6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Feb 2026 00:29:41 +0000 Subject: [PATCH 10/14] refactor: optimize schemaNameTransform for efficiency and clarity Co-authored-by: mrlubos <12529395+mrlubos@users.noreply.github.com> --- .../openapi-ts-tests/main/test/3.0.x.test.ts | 5 +- .../src/openApi/shared/transforms/schemas.ts | 52 +++---------------- 2 files changed, 9 insertions(+), 48 deletions(-) diff --git a/packages/openapi-ts-tests/main/test/3.0.x.test.ts b/packages/openapi-ts-tests/main/test/3.0.x.test.ts index 7f2d5e89f..22fff3851 100644 --- a/packages/openapi-ts-tests/main/test/3.0.x.test.ts +++ b/packages/openapi-ts-tests/main/test/3.0.x.test.ts @@ -618,11 +618,10 @@ describe(`OpenAPI ${version}`, () => { output: 'transforms-schemas-name-collision', parser: { transforms: { - schemaName: (name: string) => + schemaName: (name: string) => // Try to rename all _vX_User schemas to "User" // This should cause collisions since "User" already exists - name.replace(/_v\d+_User$/, '') - , + name.replace(/_v\d+_User$/, ''), }, }, plugins: ['@hey-api/typescript'], diff --git a/packages/shared/src/openApi/shared/transforms/schemas.ts b/packages/shared/src/openApi/shared/transforms/schemas.ts index 568bf3cc9..d89f810a3 100644 --- a/packages/shared/src/openApi/shared/transforms/schemas.ts +++ b/packages/shared/src/openApi/shared/transforms/schemas.ts @@ -8,9 +8,6 @@ type SchemaNameConfig = Parser['transforms']['schemaName']; /** * Recursively walks the entire spec object and replaces all $ref strings * according to the provided rename mapping. - * - * @param node - Current node being visited - * @param renameMap - Map from old pointer to new pointer */ const rewriteRefs = (node: unknown, renameMap: Record) => { if (node instanceof Array) { @@ -18,7 +15,6 @@ const rewriteRefs = (node: unknown, renameMap: Record) => { } else if (node && typeof node === 'object') { for (const [key, value] of Object.entries(node)) { if (key === '$ref' && typeof value === 'string' && value in renameMap) { - // Replace the $ref with the new name (node as Record)[key] = renameMap[value]; } else { rewriteRefs(value, renameMap); @@ -28,18 +24,9 @@ const rewriteRefs = (node: unknown, renameMap: Record) => { }; /** - * Applies the schema name transform to rename schema component keys and - * update all $ref pointers throughout the spec. - * - * This transform: - * 1. Iterates all schema keys in components.schemas (or definitions for Swagger 2.0) - * 2. Applies the name transformer to compute new names - * 3. Handles name collisions (skips rename if new name already exists) - * 4. Renames schema keys in the schemas object - * 5. Updates all $ref pointers throughout the spec to use the new names - * - * @param config - The schema name transformer - * @param spec - The OpenAPI spec object to transform + * Renames schema component keys and updates all $ref pointers throughout + * the spec. Handles collisions by skipping renames when the target name + * already exists or conflicts with another rename. */ export const schemaNameTransform = ({ config, @@ -62,63 +49,38 @@ export const schemaNameTransform = ({ return; } - // Build rename map: oldPointer -> newPointer const renameMap: Record = {}; const newNames = new Set(); + const namingConfig = { name: config }; - // Create a simple config object for applyNaming - const namingConfig = typeof config === 'function' ? { name: config } : { name: config }; - - // First pass: compute all new names and check for collisions for (const oldName of Object.keys(schemasObj)) { const newName = applyNaming(oldName, namingConfig); - // Skip if name doesn't change if (newName === oldName) { continue; } - // Skip if new name collides with an existing schema if (newName in schemasObj) { continue; } - // Skip if new name collides with another renamed schema if (newNames.has(newName)) { continue; } - // Record the rename renameMap[`${schemasPointerNamespace}${oldName}`] = `${schemasPointerNamespace}${newName}`; newNames.add(newName); } - // Second pass: rename schema keys - // We need to be careful about the order to avoid overwriting - const renamedSchemas: Record = {}; - const processedOldNames = new Set(); - for (const [oldPointer, newPointer] of Object.entries(renameMap)) { const oldName = oldPointer.slice(schemasPointerNamespace.length); const newName = newPointer.slice(schemasPointerNamespace.length); + const schema = schemasObj[oldName]; - // Store the schema under the new name - renamedSchemas[newName] = schemasObj[oldName]; - processedOldNames.add(oldName); - } - - // Add all schemas that weren't renamed - for (const [name, schema] of Object.entries(schemasObj)) { - if (!processedOldNames.has(name)) { - renamedSchemas[name] = schema; - } + delete schemasObj[oldName]; + schemasObj[newName] = schema; } - // Replace the entire schemas object with the renamed version - Object.keys(schemasObj).forEach((key) => delete schemasObj[key]); - Object.assign(schemasObj, renamedSchemas); - - // Third pass: rewrite all $ref pointers throughout the spec if (Object.keys(renameMap).length > 0) { rewriteRefs(spec, renameMap); } -- 2.51.2 From b20335e409ff97d3d881f0ed2a3fe71b21f7c6d8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Feb 2026 00:41:12 +0000 Subject: [PATCH 11/14] refactor: combine skip conditionals into single statement for better readability Co-authored-by: mrlubos <12529395+mrlubos@users.noreply.github.com> --- .../shared/src/openApi/shared/transforms/schemas.ts | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/packages/shared/src/openApi/shared/transforms/schemas.ts b/packages/shared/src/openApi/shared/transforms/schemas.ts index d89f810a3..adaf6a070 100644 --- a/packages/shared/src/openApi/shared/transforms/schemas.ts +++ b/packages/shared/src/openApi/shared/transforms/schemas.ts @@ -56,15 +56,7 @@ export const schemaNameTransform = ({ for (const oldName of Object.keys(schemasObj)) { const newName = applyNaming(oldName, namingConfig); - if (newName === oldName) { - continue; - } - - if (newName in schemasObj) { - continue; - } - - if (newNames.has(newName)) { + if (newName === oldName || newName in schemasObj || newNames.has(newName)) { continue; } -- 2.51.2 From 4b9d032c6a144fa79cedc28e077782b9d67803a3 Mon Sep 17 00:00:00 2001 From: Lubos Date: Thu, 19 Feb 2026 02:49:40 +0200 Subject: [PATCH 12/14] Add schemaName transform to parser Added a new `schemaName` transform to the parser. --- .changeset/quick-carrots-tie.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/quick-carrots-tie.md diff --git a/.changeset/quick-carrots-tie.md b/.changeset/quick-carrots-tie.md new file mode 100644 index 000000000..61406f6a7 --- /dev/null +++ b/.changeset/quick-carrots-tie.md @@ -0,0 +1,6 @@ +--- +"@hey-api/openapi-ts": patch +"@hey-api/shared": patch +--- + +**parser(transforms)**: add `schemaName` transform -- 2.51.2 From 81c49d7636d9a1f447cfc1038aecd289a2c25bb9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Feb 2026 00:59:46 +0000 Subject: [PATCH 13/14] fix: add null checks for regex match results to satisfy TypeScript strict mode Co-authored-by: mrlubos <12529395+mrlubos@users.noreply.github.com> --- packages/openapi-ts-tests/main/test/2.0.x.test.ts | 2 +- packages/openapi-ts-tests/main/test/3.0.x.test.ts | 2 +- packages/openapi-ts-tests/main/test/3.1.x.test.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/openapi-ts-tests/main/test/2.0.x.test.ts b/packages/openapi-ts-tests/main/test/2.0.x.test.ts index 9edb12831..916d1645a 100644 --- a/packages/openapi-ts-tests/main/test/2.0.x.test.ts +++ b/packages/openapi-ts-tests/main/test/2.0.x.test.ts @@ -300,7 +300,7 @@ describe(`OpenAPI ${version}`, () => { ); // Deduplicate prefixes: Foo_Foo → Foo const m = clean.match(/^([A-Za-z\d]+)_\1([A-Za-z\d]*)$/); - if (m) clean = m[1] + m[2]; + if (m?.[1] && m?.[2] !== undefined) clean = m[1] + m[2]; return clean; }, }, diff --git a/packages/openapi-ts-tests/main/test/3.0.x.test.ts b/packages/openapi-ts-tests/main/test/3.0.x.test.ts index 22fff3851..b39fcff4e 100644 --- a/packages/openapi-ts-tests/main/test/3.0.x.test.ts +++ b/packages/openapi-ts-tests/main/test/3.0.x.test.ts @@ -603,7 +603,7 @@ describe(`OpenAPI ${version}`, () => { ); // Deduplicate prefixes: Foo_Foo → Foo const m = clean.match(/^([A-Za-z\d]+)_\1([A-Za-z\d]*)$/); - if (m) clean = m[1] + m[2]; + if (m?.[1] && m?.[2] !== undefined) clean = m[1] + m[2]; return clean; }, }, diff --git a/packages/openapi-ts-tests/main/test/3.1.x.test.ts b/packages/openapi-ts-tests/main/test/3.1.x.test.ts index 0ed636eae..2a42d0494 100644 --- a/packages/openapi-ts-tests/main/test/3.1.x.test.ts +++ b/packages/openapi-ts-tests/main/test/3.1.x.test.ts @@ -705,7 +705,7 @@ describe(`OpenAPI ${version}`, () => { ); // Deduplicate prefixes: Foo_Foo → Foo const m = clean.match(/^([A-Za-z\d]+)_\1([A-Za-z\d]*)$/); - if (m) clean = m[1] + m[2]; + if (m?.[1] && m?.[2] !== undefined) clean = m[1] + m[2]; return clean; }, }, -- 2.51.2 From f8f44c3910f243f8baaab46da66c57db03cc0768 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Feb 2026 01:26:17 +0000 Subject: [PATCH 14/14] refactor: use non-null assertions instead of optional chaining for cleaner code Co-authored-by: mrlubos <12529395+mrlubos@users.noreply.github.com> --- packages/openapi-ts-tests/main/test/2.0.x.test.ts | 2 +- packages/openapi-ts-tests/main/test/3.0.x.test.ts | 2 +- packages/openapi-ts-tests/main/test/3.1.x.test.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/openapi-ts-tests/main/test/2.0.x.test.ts b/packages/openapi-ts-tests/main/test/2.0.x.test.ts index 916d1645a..088e3f1ae 100644 --- a/packages/openapi-ts-tests/main/test/2.0.x.test.ts +++ b/packages/openapi-ts-tests/main/test/2.0.x.test.ts @@ -300,7 +300,7 @@ describe(`OpenAPI ${version}`, () => { ); // Deduplicate prefixes: Foo_Foo → Foo const m = clean.match(/^([A-Za-z\d]+)_\1([A-Za-z\d]*)$/); - if (m?.[1] && m?.[2] !== undefined) clean = m[1] + m[2]; + if (m) clean = m[1]! + m[2]!; return clean; }, }, diff --git a/packages/openapi-ts-tests/main/test/3.0.x.test.ts b/packages/openapi-ts-tests/main/test/3.0.x.test.ts index b39fcff4e..060e78085 100644 --- a/packages/openapi-ts-tests/main/test/3.0.x.test.ts +++ b/packages/openapi-ts-tests/main/test/3.0.x.test.ts @@ -603,7 +603,7 @@ describe(`OpenAPI ${version}`, () => { ); // Deduplicate prefixes: Foo_Foo → Foo const m = clean.match(/^([A-Za-z\d]+)_\1([A-Za-z\d]*)$/); - if (m?.[1] && m?.[2] !== undefined) clean = m[1] + m[2]; + if (m) clean = m[1]! + m[2]!; return clean; }, }, diff --git a/packages/openapi-ts-tests/main/test/3.1.x.test.ts b/packages/openapi-ts-tests/main/test/3.1.x.test.ts index 2a42d0494..3700992f0 100644 --- a/packages/openapi-ts-tests/main/test/3.1.x.test.ts +++ b/packages/openapi-ts-tests/main/test/3.1.x.test.ts @@ -705,7 +705,7 @@ describe(`OpenAPI ${version}`, () => { ); // Deduplicate prefixes: Foo_Foo → Foo const m = clean.match(/^([A-Za-z\d]+)_\1([A-Za-z\d]*)$/); - if (m?.[1] && m?.[2] !== undefined) clean = m[1] + m[2]; + if (m) clean = m[1]! + m[2]!; return clean; }, },