From 39445214f6359bed07e9a8499d33298815230570 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 20 Aug 2026 16:47:54 +0000 Subject: [PATCH 01/14] Initial plan From 3cd2c7a30e44fd451b9a57d1f1c7a5b0283285b2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 20 Aug 2026 17:02:59 +0000 Subject: [PATCH 02/14] Fix OpenAPI3 conversion for component response models Co-authored-by: baywet <7905502+baywet@users.noreply.github.com> --- .../generate-response-expressions.ts | 4 + .../convert/generators/generate-types.ts | 3 + .../actions/convert/transforms/transforms.ts | 161 +++++++++++++++++- .../tsp-openapi3/convert-openapi3-doc.test.ts | 69 ++++++++ 4 files changed, 236 insertions(+), 1 deletion(-) diff --git a/packages/openapi3/src/cli/actions/convert/generators/generate-response-expressions.ts b/packages/openapi3/src/cli/actions/convert/generators/generate-response-expressions.ts index d2287a82bc1..64c1a119504 100644 --- a/packages/openapi3/src/cli/actions/convert/generators/generate-response-expressions.ts +++ b/packages/openapi3/src/cli/actions/convert/generators/generate-response-expressions.ts @@ -45,6 +45,10 @@ type GenerateReturnTypeForStatusCodeProps = { function generateReturnTypeForStatusCode(props: GenerateReturnTypeForStatusCodeProps): string[] { const { statusCode, context } = props; + if ("$ref" in props.response && props.response.$ref.startsWith("#/components/responses/")) { + return [context.getRefName(props.response.$ref, props.operationScope)]; + } + const response = "$ref" in props.response ? context.getByRef(props.response.$ref) diff --git a/packages/openapi3/src/cli/actions/convert/generators/generate-types.ts b/packages/openapi3/src/cli/actions/convert/generators/generate-types.ts index df205efec73..85f5a22209b 100644 --- a/packages/openapi3/src/cli/actions/convert/generators/generate-types.ts +++ b/packages/openapi3/src/cli/actions/convert/generators/generate-types.ts @@ -98,6 +98,9 @@ export class SchemaToExpressionGenerator { case "parameters": scopeAndName.scope.unshift("Parameters"); break; + case "responses": + scopeAndName.scope.unshift("Responses"); + break; } return scopeAndName; diff --git a/packages/openapi3/src/cli/actions/convert/transforms/transforms.ts b/packages/openapi3/src/cli/actions/convert/transforms/transforms.ts index 3ccac332d13..fa6a609d089 100644 --- a/packages/openapi3/src/cli/actions/convert/transforms/transforms.ts +++ b/packages/openapi3/src/cli/actions/convert/transforms/transforms.ts @@ -1,15 +1,25 @@ import type { + OpenAPI3Header, OpenAPI3PathItem, OpenAPI3RequestBody, + OpenAPI3Response, OpenAPI3Responses, OpenAPIPathItem3_2, OpenAPIRequestBody3_2, OpenAPIResponses3_2, Refable, SupportedOpenAPIDocuments, + SupportedOpenAPISchema, } from "../../../../types.js"; -import type { TypeSpecModel, TypeSpecProgram } from "../interfaces.js"; +import type { + TypeSpecDataTypes, + TypeSpecModel, + TypeSpecModelProperty, + TypeSpecProgram, +} from "../interfaces.js"; import type { Context } from "../utils/context.js"; +import { getScopeAndName } from "../utils/get-scope-and-name.js"; +import { convertHeaderName } from "../utils/convert-header-name.js"; import { transformComponentParameters } from "./transform-component-parameters.js"; import { transformComponentSchemas } from "./transform-component-schemas.js"; import { transformNamespaces } from "./transform-namespaces.js"; @@ -294,8 +304,157 @@ function collectDataTypes(context: Context): TypeSpecModel[] { const models: TypeSpecModel[] = []; // get models from `#/components/schema transformComponentSchemas(context, models); + transformComponentResponses(context, models); // get models from `#/components/parameters transformComponentParameters(context, models); return models; } + +export function transformComponentResponses(context: Context, dataTypes: TypeSpecDataTypes[]): void { + const responses = context.openApi3Doc.components?.responses; + if (!responses) return; + + const seenResponseRefs = new Set(); + + for (const path of Object.values(context.openApi3Doc.paths ?? {})) { + if (!path) continue; + for (const method of methods) { + const operation = path[method]; + if (!operation?.responses) continue; + + const operationResponses = (operation as any).responses as Record | undefined; + if (!operationResponses) continue; + + for (const [statusCode, response] of Object.entries(operationResponses)) { + const responseObject = response as any; + if ( + !responseObject || + typeof responseObject !== "object" || + !("$ref" in responseObject) || + typeof responseObject.$ref !== "string" || + !responseObject.$ref.startsWith("#/components/responses/") + ) { + continue; + } + + const ref = responseObject.$ref as string; + if (seenResponseRefs.has(ref)) continue; + seenResponseRefs.add(ref); + + const componentResponse = context.getByRef(ref); + if (!componentResponse) continue; + + const { name, scope } = getScopeAndName(ref.slice("#/components/responses/".length)); + const namespace = [...scope]; + namespace.unshift("Responses"); + + dataTypes.push({ + kind: "model", + name, + scope: namespace, + decorators: [], + doc: componentResponse.description, + properties: getResponseProperties(statusCode, componentResponse, context), + }); + } + } + } +} + +function getResponseProperties( + statusCode: string, + response: OpenAPI3Response, + context: Context, +): TypeSpecModelProperty[] { + const properties: TypeSpecModelProperty[] = []; + const resolvedStatus = statusCode === "default" ? "default" : statusCode; + + if (resolvedStatus !== "default") { + properties.push(convertStatusCodeToProperty(resolvedStatus)); + } + + for (const [headerName, header] of Object.entries(response.headers ?? {})) { + const property = convertHeaderToProperty({ name: headerName, header, context }); + if (property) { + properties.push(property); + } + } + + const contentEntries = Object.entries(response.content ?? {}); + const preferredBodySchema = [ + contentEntries.find(([mediaType]) => mediaType === "application/json"), + contentEntries[0], + ].find((entry): entry is [string, any] => !!entry)?.[1]; + + const bodySchema = + preferredBodySchema && typeof preferredBodySchema === "object" && "schema" in preferredBodySchema + ? (preferredBodySchema.schema as Refable) + : undefined; + + if (bodySchema) { + properties.push({ + name: "body", + decorators: [{ name: "body", args: [] }], + isOptional: false, + schema: bodySchema, + }); + } + + return properties; +} + +function convertStatusCodeToProperty(statusCode: string): TypeSpecModelProperty { + const schema: SupportedOpenAPISchema = { type: "integer", format: "int32" }; + + if (statusCode === "1XX") { + schema.minimum = 100; + schema.maximum = 199; + } else if (statusCode === "2XX") { + schema.minimum = 200; + schema.maximum = 299; + } else if (statusCode === "3XX") { + schema.minimum = 300; + schema.maximum = 399; + } else if (statusCode === "4XX") { + schema.minimum = 400; + schema.maximum = 499; + } else if (statusCode === "5XX") { + schema.minimum = 500; + schema.maximum = 599; + } else if (/^[1-5][0-9]{2}$/.test(statusCode)) { + schema.enum = [Number.parseInt(statusCode, 10)]; + } + + return { + name: "statusCode", + schema, + decorators: [{ name: "statusCode", args: [] }], + isOptional: false, + }; +} + +function convertHeaderToProperty(props: { + name: string; + header: Refable; + context: Context; +}): TypeSpecModelProperty | undefined { + const { name, context } = props; + const header = "$ref" in props.header ? context.getByRef(props.header.$ref) : props.header; + + if (!header) return undefined; + + const normalizedName = convertHeaderName(name); + const decorator = { name: "header", args: [] as (string | number | object)[] }; + if (normalizedName !== name) { + decorator.args.push(name); + } + + return { + name: normalizedName, + decorators: [decorator], + doc: header.description, + isOptional: !header.required, + schema: header.schema ?? {}, + }; +} diff --git a/packages/openapi3/test/tsp-openapi3/convert-openapi3-doc.test.ts b/packages/openapi3/test/tsp-openapi3/convert-openapi3-doc.test.ts index f4e6dd83380..8ced0eff85f 100644 --- a/packages/openapi3/test/tsp-openapi3/convert-openapi3-doc.test.ts +++ b/packages/openapi3/test/tsp-openapi3/convert-openapi3-doc.test.ts @@ -87,6 +87,75 @@ describe.each(versions)("convertOpenAPI3Document v%s", (version) => { ); }); + it("creates reusable response models for referenced component responses", async () => { + const tsp = await convertOpenAPI3Document({ + openapi: version, + info: { + title: "Example API", + version: "1.0.0", + }, + paths: { + "/endpoint": { + get: { + operationId: "endpoint", + responses: { + "429": { + $ref: "#/components/responses/TooManyRequests", + }, + }, + }, + }, + }, + components: { + responses: { + TooManyRequests: { + description: "The request was rejected because a rate limit was exceeded.", + headers: { + "Retry-After": { + description: "Retry delay seconds.", + required: false, + schema: { + type: "integer", + minimum: 1, + }, + }, + }, + content: { + "application/json": { + schema: { + $ref: "#/components/schemas/ErrorResponse", + }, + }, + }, + }, + }, + schemas: { + ErrorResponse: { + type: "object", + properties: { + message: { + type: "string", + description: "A human-readable message.", + }, + }, + }, + }, + }, + } as any); + + strictEqual(tsp.includes("namespace Responses"), true, "Expected generated response namespace: " + tsp); + strictEqual(tsp.includes("model TooManyRequests {"), true, "Expected response model: " + tsp); + strictEqual(tsp.includes("@statusCode statusCode"), true, "Expected status code property: " + tsp); + strictEqual(tsp.includes('@header("Retry-After")'), true, "Expected retry-after header: " + tsp); + strictEqual(tsp.includes("@body body:"), true, "Expected response body property: " + tsp); + strictEqual(tsp.includes("ErrorResponse"), true, "Expected body schema reference: " + tsp); + strictEqual( + tsp.includes("op endpoint(): Responses.TooManyRequests;"), + true, + "Expected operation to use the generated response model: " + tsp, + ); + }); + describe("Union types with multiple defaults", () => { it("should select first default for union types with multiple defaults", async () => { const tsp = await convertOpenAPI3Document({ From 3817837fc161c458e3242380d2df650e62961d7d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 20 Aug 2026 17:31:38 +0000 Subject: [PATCH 03/14] Add changelog entry for openapi3 component response conversion fix Co-authored-by: baywet <7905502+baywet@users.noreply.github.com> --- .../changes/fix-openapi3-component-responses-2026-8-20.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .chronus/changes/fix-openapi3-component-responses-2026-8-20.md diff --git a/.chronus/changes/fix-openapi3-component-responses-2026-8-20.md b/.chronus/changes/fix-openapi3-component-responses-2026-8-20.md new file mode 100644 index 00000000000..33d671040ab --- /dev/null +++ b/.chronus/changes/fix-openapi3-component-responses-2026-8-20.md @@ -0,0 +1,7 @@ +--- +changeKind: fix +packages: + - "@typespec/openapi3" +--- + +[converter] Emit reusable models under a `Responses` namespace for `#/components/responses/...` references instead of inlining the response at each operation From 7a7f1ed4898324d8e8ba7411707121ecfaa7318a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 20 Aug 2026 17:41:21 +0000 Subject: [PATCH 04/14] test: cover component response dedupe across multiple operations Co-authored-by: baywet <7905502+baywet@users.noreply.github.com> --- .../tsp-openapi3/convert-openapi3-doc.test.ts | 38 +++++++++++++++++-- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/packages/openapi3/test/tsp-openapi3/convert-openapi3-doc.test.ts b/packages/openapi3/test/tsp-openapi3/convert-openapi3-doc.test.ts index 8ced0eff85f..f97b000bcca 100644 --- a/packages/openapi3/test/tsp-openapi3/convert-openapi3-doc.test.ts +++ b/packages/openapi3/test/tsp-openapi3/convert-openapi3-doc.test.ts @@ -105,6 +105,16 @@ describe.each(versions)("convertOpenAPI3Document v%s", (version) => { }, }, }, + "/other-endpoint": { + get: { + operationId: "otherEndpoint", + responses: { + "429": { + $ref: "#/components/responses/TooManyRequests", + }, + }, + }, + }, }, components: { responses: { @@ -143,10 +153,22 @@ describe.each(versions)("convertOpenAPI3Document v%s", (version) => { }, } as any); - strictEqual(tsp.includes("namespace Responses"), true, "Expected generated response namespace: " + tsp); + strictEqual( + tsp.includes("namespace Responses"), + true, + "Expected generated response namespace: " + tsp, + ); strictEqual(tsp.includes("model TooManyRequests {"), true, "Expected response model: " + tsp); - strictEqual(tsp.includes("@statusCode statusCode"), true, "Expected status code property: " + tsp); - strictEqual(tsp.includes('@header("Retry-After")'), true, "Expected retry-after header: " + tsp); + strictEqual( + tsp.includes("@statusCode statusCode"), + true, + "Expected status code property: " + tsp, + ); + strictEqual( + tsp.includes('@header("Retry-After")'), + true, + "Expected retry-after header: " + tsp, + ); strictEqual(tsp.includes("@body body:"), true, "Expected response body property: " + tsp); strictEqual(tsp.includes("ErrorResponse"), true, "Expected body schema reference: " + tsp); strictEqual( @@ -154,6 +176,16 @@ describe.each(versions)("convertOpenAPI3Document v%s", (version) => { true, "Expected operation to use the generated response model: " + tsp, ); + strictEqual( + tsp.includes("op otherEndpoint(): Responses.TooManyRequests;"), + true, + "Expected second operation to use the generated response model: " + tsp, + ); + strictEqual( + tsp.split("model TooManyRequests {").length - 1, + 1, + "Expected the shared component response model to be generated once: " + tsp, + ); }); describe("Union types with multiple defaults", () => { From 20db7f3646fdee27768abdabc49efdabda21dab2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 20 Aug 2026 17:41:30 +0000 Subject: [PATCH 05/14] refactor: share status code/header property conversion helpers Co-authored-by: baywet <7905502+baywet@users.noreply.github.com> --- .../generate-response-expressions.ts | 88 ++----------------- .../actions/convert/transforms/transforms.ts | 70 +++------------ .../convert/utils/response-properties.ts | 76 ++++++++++++++++ 3 files changed, 95 insertions(+), 139 deletions(-) create mode 100644 packages/openapi3/src/cli/actions/convert/utils/response-properties.ts diff --git a/packages/openapi3/src/cli/actions/convert/generators/generate-response-expressions.ts b/packages/openapi3/src/cli/actions/convert/generators/generate-response-expressions.ts index 64c1a119504..d97b3fa8e83 100644 --- a/packages/openapi3/src/cli/actions/convert/generators/generate-response-expressions.ts +++ b/packages/openapi3/src/cli/actions/convert/generators/generate-response-expressions.ts @@ -1,18 +1,14 @@ -import type { - OpenAPI3Header, - OpenAPI3MediaType, - OpenAPI3Response, - OpenAPI3Schema, - Refable, -} from "../../../../types.js"; -import type { TypeSpecDecorator, TypeSpecModelProperty, TypeSpecOperation } from "../interfaces.js"; +import type { OpenAPI3MediaType, OpenAPI3Response, Refable } from "../../../../types.js"; +import type { TypeSpecModelProperty, TypeSpecOperation } from "../interfaces.js"; import type { Context } from "../utils/context.js"; -import { convertHeaderName } from "../utils/convert-header-name.js"; -import { getDecoratorsForSchema } from "../utils/decorators.js"; +import type { StatusCodes } from "../utils/response-properties.js"; +import { + convertHeaderToProperty, + convertStatusCodeToProperty, + isValidLiteralStatusCode, +} from "../utils/response-properties.js"; import { generateModelExpression } from "./generate-model.js"; -type StatusCodes = string | "1XX" | "2XX" | "3XX" | "4XX" | "5XX" | "default"; - /** * Generates a union expression of all possible responses for an operation */ @@ -322,74 +318,6 @@ function generateDefaultResponse({ return `GeneratedHelpers.DefaultResponse<${description}${headers}${body}>`; } -function convertStatusCodeToProperty( - statusCode: Exclude, -): TypeSpecModelProperty { - const schema: OpenAPI3Schema = { type: "integer", format: "int32" }; - if (statusCode === "1XX") { - schema.minimum = 100; - schema.maximum = 199; - } else if (statusCode === "2XX") { - schema.minimum = 200; - schema.maximum = 299; - } else if (statusCode === "3XX") { - schema.minimum = 300; - schema.maximum = 399; - } else if (statusCode === "4XX") { - schema.minimum = 400; - schema.maximum = 499; - } else if (statusCode === "5XX") { - schema.minimum = 500; - schema.maximum = 599; - } else if (isValidLiteralStatusCode(statusCode)) { - const literalStatusCode = parseInt(statusCode, 10); - schema.enum = [literalStatusCode]; - } - return { - name: "statusCode", - schema, - decorators: [{ name: "statusCode", args: [] }], - isOptional: false, - }; -} - -function isValidLiteralStatusCode(statusCode: StatusCodes): boolean { - if (statusCode === "default" || statusCode.endsWith("X")) return false; - - const literalStatusCode = parseInt(statusCode, 10); - return isFinite(literalStatusCode) && literalStatusCode >= 100 && literalStatusCode <= 599; -} - -type ConvertHeaderToPropertyProps = { - name: string; - header: Refable; - context: Context; -}; -function convertHeaderToProperty( - props: ConvertHeaderToPropertyProps, -): TypeSpecModelProperty | undefined { - const { name, context } = props; - const header = - "$ref" in props.header ? context.getByRef(props.header.$ref) : props.header; - - if (!header) return; - - const normalizedName = convertHeaderName(name); - // TODO: handle style - const headerDecorator: TypeSpecDecorator = { name: "header", args: [] }; - if (normalizedName !== name) { - headerDecorator.args.push(name); - } - - return { - name: normalizedName, - decorators: [headerDecorator, ...(header.schema ? getDecoratorsForSchema(header.schema) : [])], - doc: props.header.description ?? header.description ?? header.schema?.description, - isOptional: !header.required, - schema: header.schema ?? {}, - }; -} - // Map of statusCodes to their Response const statusCodeToResponse = new Map([ [200, "OkResponse"], diff --git a/packages/openapi3/src/cli/actions/convert/transforms/transforms.ts b/packages/openapi3/src/cli/actions/convert/transforms/transforms.ts index fa6a609d089..5b0be9f5568 100644 --- a/packages/openapi3/src/cli/actions/convert/transforms/transforms.ts +++ b/packages/openapi3/src/cli/actions/convert/transforms/transforms.ts @@ -1,5 +1,4 @@ import type { - OpenAPI3Header, OpenAPI3PathItem, OpenAPI3RequestBody, OpenAPI3Response, @@ -19,7 +18,10 @@ import type { } from "../interfaces.js"; import type { Context } from "../utils/context.js"; import { getScopeAndName } from "../utils/get-scope-and-name.js"; -import { convertHeaderName } from "../utils/convert-header-name.js"; +import { + convertHeaderToProperty, + convertStatusCodeToProperty, +} from "../utils/response-properties.js"; import { transformComponentParameters } from "./transform-component-parameters.js"; import { transformComponentSchemas } from "./transform-component-schemas.js"; import { transformNamespaces } from "./transform-namespaces.js"; @@ -311,7 +313,10 @@ function collectDataTypes(context: Context): TypeSpecModel[] { return models; } -export function transformComponentResponses(context: Context, dataTypes: TypeSpecDataTypes[]): void { +export function transformComponentResponses( + context: Context, + dataTypes: TypeSpecDataTypes[], +): void { const responses = context.openApi3Doc.components?.responses; if (!responses) return; @@ -388,7 +393,9 @@ function getResponseProperties( ].find((entry): entry is [string, any] => !!entry)?.[1]; const bodySchema = - preferredBodySchema && typeof preferredBodySchema === "object" && "schema" in preferredBodySchema + preferredBodySchema && + typeof preferredBodySchema === "object" && + "schema" in preferredBodySchema ? (preferredBodySchema.schema as Refable) : undefined; @@ -403,58 +410,3 @@ function getResponseProperties( return properties; } - -function convertStatusCodeToProperty(statusCode: string): TypeSpecModelProperty { - const schema: SupportedOpenAPISchema = { type: "integer", format: "int32" }; - - if (statusCode === "1XX") { - schema.minimum = 100; - schema.maximum = 199; - } else if (statusCode === "2XX") { - schema.minimum = 200; - schema.maximum = 299; - } else if (statusCode === "3XX") { - schema.minimum = 300; - schema.maximum = 399; - } else if (statusCode === "4XX") { - schema.minimum = 400; - schema.maximum = 499; - } else if (statusCode === "5XX") { - schema.minimum = 500; - schema.maximum = 599; - } else if (/^[1-5][0-9]{2}$/.test(statusCode)) { - schema.enum = [Number.parseInt(statusCode, 10)]; - } - - return { - name: "statusCode", - schema, - decorators: [{ name: "statusCode", args: [] }], - isOptional: false, - }; -} - -function convertHeaderToProperty(props: { - name: string; - header: Refable; - context: Context; -}): TypeSpecModelProperty | undefined { - const { name, context } = props; - const header = "$ref" in props.header ? context.getByRef(props.header.$ref) : props.header; - - if (!header) return undefined; - - const normalizedName = convertHeaderName(name); - const decorator = { name: "header", args: [] as (string | number | object)[] }; - if (normalizedName !== name) { - decorator.args.push(name); - } - - return { - name: normalizedName, - decorators: [decorator], - doc: header.description, - isOptional: !header.required, - schema: header.schema ?? {}, - }; -} diff --git a/packages/openapi3/src/cli/actions/convert/utils/response-properties.ts b/packages/openapi3/src/cli/actions/convert/utils/response-properties.ts new file mode 100644 index 00000000000..6d9d2d5f634 --- /dev/null +++ b/packages/openapi3/src/cli/actions/convert/utils/response-properties.ts @@ -0,0 +1,76 @@ +import type { OpenAPI3Header, OpenAPI3Schema, Refable } from "../../../../types.js"; +import type { TypeSpecDecorator, TypeSpecModelProperty } from "../interfaces.js"; +import type { Context } from "./context.js"; +import { convertHeaderName } from "./convert-header-name.js"; +import { getDecoratorsForSchema } from "./decorators.js"; + +export type StatusCodes = string | "1XX" | "2XX" | "3XX" | "4XX" | "5XX" | "default"; + +export function isValidLiteralStatusCode(statusCode: StatusCodes): boolean { + if (statusCode === "default" || statusCode.endsWith("X")) return false; + + const literalStatusCode = parseInt(statusCode, 10); + return isFinite(literalStatusCode) && literalStatusCode >= 100 && literalStatusCode <= 599; +} + +export function convertStatusCodeToProperty( + statusCode: Exclude, +): TypeSpecModelProperty { + const schema: OpenAPI3Schema = { type: "integer", format: "int32" }; + if (statusCode === "1XX") { + schema.minimum = 100; + schema.maximum = 199; + } else if (statusCode === "2XX") { + schema.minimum = 200; + schema.maximum = 299; + } else if (statusCode === "3XX") { + schema.minimum = 300; + schema.maximum = 399; + } else if (statusCode === "4XX") { + schema.minimum = 400; + schema.maximum = 499; + } else if (statusCode === "5XX") { + schema.minimum = 500; + schema.maximum = 599; + } else if (isValidLiteralStatusCode(statusCode)) { + const literalStatusCode = parseInt(statusCode, 10); + schema.enum = [literalStatusCode]; + } + return { + name: "statusCode", + schema, + decorators: [{ name: "statusCode", args: [] }], + isOptional: false, + }; +} + +export type ConvertHeaderToPropertyProps = { + name: string; + header: Refable; + context: Context; +}; + +export function convertHeaderToProperty( + props: ConvertHeaderToPropertyProps, +): TypeSpecModelProperty | undefined { + const { name, context } = props; + const header = + "$ref" in props.header ? context.getByRef(props.header.$ref) : props.header; + + if (!header) return; + + const normalizedName = convertHeaderName(name); + // TODO: handle style + const headerDecorator: TypeSpecDecorator = { name: "header", args: [] }; + if (normalizedName !== name) { + headerDecorator.args.push(name); + } + + return { + name: normalizedName, + decorators: [headerDecorator, ...(header.schema ? getDecoratorsForSchema(header.schema) : [])], + doc: props.header.description ?? header.description ?? header.schema?.description, + isOptional: !header.required, + schema: header.schema ?? {}, + }; +} From 9f353e96ca37a84b485e594be48002c99f6b0d31 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 20 Aug 2026 17:41:53 +0000 Subject: [PATCH 06/14] Only reuse component response models when status code matches Co-authored-by: baywet <7905502+baywet@users.noreply.github.com> --- .../generate-response-expressions.ts | 6 +++++- .../actions/convert/transforms/transforms.ts | 5 +++++ .../src/cli/actions/convert/utils/context.ts | 19 +++++++++++++++++++ 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/packages/openapi3/src/cli/actions/convert/generators/generate-response-expressions.ts b/packages/openapi3/src/cli/actions/convert/generators/generate-response-expressions.ts index d97b3fa8e83..9f48b6d908c 100644 --- a/packages/openapi3/src/cli/actions/convert/generators/generate-response-expressions.ts +++ b/packages/openapi3/src/cli/actions/convert/generators/generate-response-expressions.ts @@ -41,7 +41,11 @@ type GenerateReturnTypeForStatusCodeProps = { function generateReturnTypeForStatusCode(props: GenerateReturnTypeForStatusCodeProps): string[] { const { statusCode, context } = props; - if ("$ref" in props.response && props.response.$ref.startsWith("#/components/responses/")) { + if ( + "$ref" in props.response && + props.response.$ref.startsWith("#/components/responses/") && + context.getComponentResponseStatusCode(props.response.$ref) === statusCode + ) { return [context.getRefName(props.response.$ref, props.operationScope)]; } diff --git a/packages/openapi3/src/cli/actions/convert/transforms/transforms.ts b/packages/openapi3/src/cli/actions/convert/transforms/transforms.ts index 5b0be9f5568..874e8e1191d 100644 --- a/packages/openapi3/src/cli/actions/convert/transforms/transforms.ts +++ b/packages/openapi3/src/cli/actions/convert/transforms/transforms.ts @@ -350,6 +350,11 @@ export function transformComponentResponses( const componentResponse = context.getByRef(ref); if (!componentResponse) continue; + // The generated model bakes in the status code of the operation response it was + // first encountered with. Record it so responses using the same component under a + // different status code can be generated inline instead of reusing this model. + context.registerComponentResponseStatusCode(ref, statusCode); + const { name, scope } = getScopeAndName(ref.slice("#/components/responses/".length)); const namespace = [...scope]; namespace.unshift("Responses"); diff --git a/packages/openapi3/src/cli/actions/convert/utils/context.ts b/packages/openapi3/src/cli/actions/convert/utils/context.ts index 74c82c5f651..bf8da2de30c 100644 --- a/packages/openapi3/src/cli/actions/convert/utils/context.ts +++ b/packages/openapi3/src/cli/actions/convert/utils/context.ts @@ -72,6 +72,16 @@ export interface Context { */ isErrorResponseSchema(ref: string): boolean; + /** + * Register the status code a component response model was generated for. + */ + registerComponentResponseStatusCode(ref: string, statusCode: string): void; + + /** + * Get the status code a component response model was generated for, if any. + */ + getComponentResponseStatusCode(ref: string): string | undefined; + /** * Mark that SSE features are being used, which will trigger including SSE-related imports. */ @@ -102,6 +112,9 @@ export function createContext( // Track schemas that are used as error response bodies const errorResponseSchemas = new Set(); + // Track the status code each generated component response model was created for + const componentResponseStatusCodes = new Map(); + // Track if SSE features are used let sseUsed = false; @@ -205,6 +218,12 @@ export function createContext( isErrorResponseSchema(ref: string): boolean { return errorResponseSchemas.has(ref); }, + registerComponentResponseStatusCode(ref: string, statusCode: string) { + componentResponseStatusCodes.set(ref, statusCode); + }, + getComponentResponseStatusCode(ref: string): string | undefined { + return componentResponseStatusCodes.get(ref); + }, markSSEUsage() { sseUsed = true; }, From 1d95e5dd8f23dc6aafe39d28a0c4fa7f3c2dad50 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 20 Aug 2026 17:44:11 +0000 Subject: [PATCH 07/14] Add test for component response reused under a different status code Co-authored-by: baywet <7905502+baywet@users.noreply.github.com> --- .../tsp-openapi3/convert-openapi3-doc.test.ts | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/packages/openapi3/test/tsp-openapi3/convert-openapi3-doc.test.ts b/packages/openapi3/test/tsp-openapi3/convert-openapi3-doc.test.ts index f97b000bcca..b76c07dc4c5 100644 --- a/packages/openapi3/test/tsp-openapi3/convert-openapi3-doc.test.ts +++ b/packages/openapi3/test/tsp-openapi3/convert-openapi3-doc.test.ts @@ -188,6 +188,68 @@ describe.each(versions)("convertOpenAPI3Document v%s", (version) => { ); }); + it("does not reuse a component response model for a different status code", async () => { + const tsp = await convertOpenAPI3Document({ + openapi: version, + info: { + title: "Example API", + version: "1.0.0", + }, + paths: { + "/endpoint": { + get: { + operationId: "endpoint", + responses: { + "429": { + $ref: "#/components/responses/Rejected", + }, + }, + }, + }, + "/other-endpoint": { + get: { + operationId: "otherEndpoint", + responses: { + "503": { + $ref: "#/components/responses/Rejected", + }, + }, + }, + }, + }, + components: { + responses: { + Rejected: { + description: "The request was rejected.", + content: { + "application/json": { + schema: { + type: "string", + }, + }, + }, + }, + }, + }, + } as any); + + strictEqual( + tsp.includes("op endpoint(): Responses.Rejected;"), + true, + "Expected first operation to use the generated response model: " + tsp, + ); + strictEqual( + tsp.includes("op otherEndpoint(): Responses.Rejected;"), + false, + "Expected second operation to not reuse the 429 response model: " + tsp, + ); + strictEqual( + tsp.includes("@statusCode statusCode: 503;"), + true, + "Expected second operation to be generated inline with its own status code: " + tsp, + ); + }); + describe("Union types with multiple defaults", () => { it("should select first default for union types with multiple defaults", async () => { const tsp = await convertOpenAPI3Document({ From f0ae2ffaaf0ed1eeb9f64d09e2a18c8794a7df7a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 24 Aug 2026 12:43:57 +0000 Subject: [PATCH 08/14] fix(openapi3): reuse component responses for every status kind Co-authored-by: baywet <7905502+baywet@users.noreply.github.com> --- .../generate-response-expressions.ts | 11 ++-- .../actions/convert/transforms/transforms.ts | 47 ++++++++++------ .../src/cli/actions/convert/utils/context.ts | 20 +++---- .../tsp-openapi3/convert-openapi3-doc.test.ts | 54 ++++++++++++++++--- 4 files changed, 94 insertions(+), 38 deletions(-) diff --git a/packages/openapi3/src/cli/actions/convert/generators/generate-response-expressions.ts b/packages/openapi3/src/cli/actions/convert/generators/generate-response-expressions.ts index 9f48b6d908c..51b0b0e5ad2 100644 --- a/packages/openapi3/src/cli/actions/convert/generators/generate-response-expressions.ts +++ b/packages/openapi3/src/cli/actions/convert/generators/generate-response-expressions.ts @@ -41,12 +41,11 @@ type GenerateReturnTypeForStatusCodeProps = { function generateReturnTypeForStatusCode(props: GenerateReturnTypeForStatusCodeProps): string[] { const { statusCode, context } = props; - if ( - "$ref" in props.response && - props.response.$ref.startsWith("#/components/responses/") && - context.getComponentResponseStatusCode(props.response.$ref) === statusCode - ) { - return [context.getRefName(props.response.$ref, props.operationScope)]; + if ("$ref" in props.response && props.response.$ref.startsWith("#/components/responses/")) { + const componentResponseName = context.getComponentResponseName(props.response.$ref, statusCode); + if (componentResponseName) { + return [componentResponseName]; + } } const response = diff --git a/packages/openapi3/src/cli/actions/convert/transforms/transforms.ts b/packages/openapi3/src/cli/actions/convert/transforms/transforms.ts index 874e8e1191d..e6c5e9f0f6d 100644 --- a/packages/openapi3/src/cli/actions/convert/transforms/transforms.ts +++ b/packages/openapi3/src/cli/actions/convert/transforms/transforms.ts @@ -321,6 +321,7 @@ export function transformComponentResponses( if (!responses) return; const seenResponseRefs = new Set(); + const componentResponseNames = new Set(); for (const path of Object.values(context.openApi3Doc.paths ?? {})) { if (!path) continue; @@ -344,26 +345,35 @@ export function transformComponentResponses( } const ref = responseObject.$ref as string; - if (seenResponseRefs.has(ref)) continue; - seenResponseRefs.add(ref); + const responseKey = `${ref}:${statusCode}`; + if (seenResponseRefs.has(responseKey)) continue; + seenResponseRefs.add(responseKey); const componentResponse = context.getByRef(ref); if (!componentResponse) continue; - // The generated model bakes in the status code of the operation response it was - // first encountered with. Record it so responses using the same component under a - // different status code can be generated inline instead of reusing this model. - context.registerComponentResponseStatusCode(ref, statusCode); - const { name, scope } = getScopeAndName(ref.slice("#/components/responses/".length)); const namespace = [...scope]; namespace.unshift("Responses"); + const nameSuffix = statusCode === "default" ? "Default" : statusCode; + let componentResponseName = name; + let index = 1; + while (componentResponseNames.has([...namespace, componentResponseName].join("."))) { + componentResponseName = `${name}${nameSuffix}${index === 1 ? "" : index}`; + index++; + } + componentResponseNames.add([...namespace, componentResponseName].join(".")); + context.registerComponentResponse( + ref, + statusCode, + [...namespace, componentResponseName].join("."), + ); dataTypes.push({ kind: "model", - name, + name: componentResponseName, scope: namespace, - decorators: [], + decorators: statusCode === "default" ? [{ name: "error", args: [] }] : [], doc: componentResponse.description, properties: getResponseProperties(statusCode, componentResponse, context), }); @@ -392,18 +402,25 @@ function getResponseProperties( } const contentEntries = Object.entries(response.content ?? {}); - const preferredBodySchema = [ + const preferredContent = [ contentEntries.find(([mediaType]) => mediaType === "application/json"), contentEntries[0], - ].find((entry): entry is [string, any] => !!entry)?.[1]; + ].find((entry): entry is [string, any] => !!entry); const bodySchema = - preferredBodySchema && - typeof preferredBodySchema === "object" && - "schema" in preferredBodySchema - ? (preferredBodySchema.schema as Refable) + preferredContent && typeof preferredContent[1] === "object" && "schema" in preferredContent[1] + ? (preferredContent[1].schema as Refable) : undefined; + if (preferredContent && preferredContent[0] !== "application/json") { + properties.push({ + name: "contentType", + decorators: [{ name: "header", args: [] }], + isOptional: false, + schema: { type: "string", enum: [preferredContent[0]] }, + }); + } + if (bodySchema) { properties.push({ name: "body", diff --git a/packages/openapi3/src/cli/actions/convert/utils/context.ts b/packages/openapi3/src/cli/actions/convert/utils/context.ts index bf8da2de30c..6bfe7167f65 100644 --- a/packages/openapi3/src/cli/actions/convert/utils/context.ts +++ b/packages/openapi3/src/cli/actions/convert/utils/context.ts @@ -73,14 +73,14 @@ export interface Context { isErrorResponseSchema(ref: string): boolean; /** - * Register the status code a component response model was generated for. + * Register a generated component response model. */ - registerComponentResponseStatusCode(ref: string, statusCode: string): void; + registerComponentResponse(ref: string, statusCode: string, name: string): void; /** - * Get the status code a component response model was generated for, if any. + * Get the generated model name for a component response and status code, if any. */ - getComponentResponseStatusCode(ref: string): string | undefined; + getComponentResponseName(ref: string, statusCode: string): string | undefined; /** * Mark that SSE features are being used, which will trigger including SSE-related imports. @@ -112,8 +112,8 @@ export function createContext( // Track schemas that are used as error response bodies const errorResponseSchemas = new Set(); - // Track the status code each generated component response model was created for - const componentResponseStatusCodes = new Map(); + // Track generated component response models by response reference and status code + const componentResponseNames = new Map(); // Track if SSE features are used let sseUsed = false; @@ -218,11 +218,11 @@ export function createContext( isErrorResponseSchema(ref: string): boolean { return errorResponseSchemas.has(ref); }, - registerComponentResponseStatusCode(ref: string, statusCode: string) { - componentResponseStatusCodes.set(ref, statusCode); + registerComponentResponse(ref: string, statusCode: string, name: string) { + componentResponseNames.set(`${ref}:${statusCode}`, name); }, - getComponentResponseStatusCode(ref: string): string | undefined { - return componentResponseStatusCodes.get(ref); + getComponentResponseName(ref: string, statusCode: string): string | undefined { + return componentResponseNames.get(`${ref}:${statusCode}`); }, markSSEUsage() { sseUsed = true; diff --git a/packages/openapi3/test/tsp-openapi3/convert-openapi3-doc.test.ts b/packages/openapi3/test/tsp-openapi3/convert-openapi3-doc.test.ts index b76c07dc4c5..3fe81b558a4 100644 --- a/packages/openapi3/test/tsp-openapi3/convert-openapi3-doc.test.ts +++ b/packages/openapi3/test/tsp-openapi3/convert-openapi3-doc.test.ts @@ -188,7 +188,7 @@ describe.each(versions)("convertOpenAPI3Document v%s", (version) => { ); }); - it("does not reuse a component response model for a different status code", async () => { + it("creates reusable component response models for all status code kinds", async () => { const tsp = await convertOpenAPI3Document({ openapi: version, info: { @@ -209,6 +209,26 @@ describe.each(versions)("convertOpenAPI3Document v%s", (version) => { "/other-endpoint": { get: { operationId: "otherEndpoint", + responses: { + "4XX": { + $ref: "#/components/responses/Rejected", + }, + }, + }, + }, + "/default-endpoint": { + get: { + operationId: "defaultEndpoint", + responses: { + default: { + $ref: "#/components/responses/Rejected", + }, + }, + }, + }, + "/unavailable-endpoint": { + get: { + operationId: "unavailableEndpoint", responses: { "503": { $ref: "#/components/responses/Rejected", @@ -222,7 +242,7 @@ describe.each(versions)("convertOpenAPI3Document v%s", (version) => { Rejected: { description: "The request was rejected.", content: { - "application/json": { + "application/xml": { schema: { type: "string", }, @@ -239,14 +259,34 @@ describe.each(versions)("convertOpenAPI3Document v%s", (version) => { "Expected first operation to use the generated response model: " + tsp, ); strictEqual( - tsp.includes("op otherEndpoint(): Responses.Rejected;"), - false, - "Expected second operation to not reuse the 429 response model: " + tsp, + tsp.includes("op otherEndpoint(): Responses.Rejected4XX;"), + true, + "Expected range response to use a generated component response model: " + tsp, + ); + strictEqual( + tsp.includes("op defaultEndpoint(): Responses.RejectedDefault;"), + true, + "Expected default response to use a generated component response model: " + tsp, + ); + strictEqual( + tsp.includes("op unavailableEndpoint(): Responses.Rejected503;"), + true, + "Expected literal response to use a generated component response model: " + tsp, + ); + strictEqual( + tsp.includes("@statusCode\n @minValue(400)\n @maxValue(499)"), + true, + "Expected range model to retain its status code: " + tsp, + ); + strictEqual( + tsp.includes("@error\n model RejectedDefault"), + true, + "Expected default response model to be marked as an error: " + tsp, ); strictEqual( - tsp.includes("@statusCode statusCode: 503;"), + tsp.includes('@header contentType: "application/xml";'), true, - "Expected second operation to be generated inline with its own status code: " + tsp, + "Expected component response models to retain the content type: " + tsp, ); }); From 0f9df8c4273fd70e707912abef9e3ae9c697745a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 24 Aug 2026 12:47:00 +0000 Subject: [PATCH 09/14] fix(openapi3): avoid component response name collisions Co-authored-by: baywet <7905502+baywet@users.noreply.github.com> --- .../openapi3/src/cli/actions/convert/transforms/transforms.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/openapi3/src/cli/actions/convert/transforms/transforms.ts b/packages/openapi3/src/cli/actions/convert/transforms/transforms.ts index e6c5e9f0f6d..b016dad979f 100644 --- a/packages/openapi3/src/cli/actions/convert/transforms/transforms.ts +++ b/packages/openapi3/src/cli/actions/convert/transforms/transforms.ts @@ -357,9 +357,9 @@ export function transformComponentResponses( namespace.unshift("Responses"); const nameSuffix = statusCode === "default" ? "Default" : statusCode; let componentResponseName = name; - let index = 1; + let index = 0; while (componentResponseNames.has([...namespace, componentResponseName].join("."))) { - componentResponseName = `${name}${nameSuffix}${index === 1 ? "" : index}`; + componentResponseName = `${name}${nameSuffix}${index || ""}`; index++; } componentResponseNames.add([...namespace, componentResponseName].join(".")); From 8a336d10be8a6bf4d857deb549d63b8795cfdf73 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Mon, 24 Aug 2026 10:52:25 -0400 Subject: [PATCH 10/14] chore: use literal instead of string Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../src/cli/actions/convert/utils/response-properties.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/openapi3/src/cli/actions/convert/utils/response-properties.ts b/packages/openapi3/src/cli/actions/convert/utils/response-properties.ts index 6d9d2d5f634..8a2a859da65 100644 --- a/packages/openapi3/src/cli/actions/convert/utils/response-properties.ts +++ b/packages/openapi3/src/cli/actions/convert/utils/response-properties.ts @@ -4,7 +4,9 @@ import type { Context } from "./context.js"; import { convertHeaderName } from "./convert-header-name.js"; import { getDecoratorsForSchema } from "./decorators.js"; -export type StatusCodes = string | "1XX" | "2XX" | "3XX" | "4XX" | "5XX" | "default"; +export type LiteralStatusCode = `${number}`; +export type WildcardStatusCode = "1XX" | "2XX" | "3XX" | "4XX" | "5XX"; +export type StatusCodes = LiteralStatusCode | WildcardStatusCode | "default"; export function isValidLiteralStatusCode(statusCode: StatusCodes): boolean { if (statusCode === "default" || statusCode.endsWith("X")) return false; From 983095cd4c0262231adb52a99bc6ceae6410b418 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Mon, 24 Aug 2026 10:52:54 -0400 Subject: [PATCH 11/14] chore: make test value return line independant Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../openapi3/test/tsp-openapi3/convert-openapi3-doc.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/openapi3/test/tsp-openapi3/convert-openapi3-doc.test.ts b/packages/openapi3/test/tsp-openapi3/convert-openapi3-doc.test.ts index 3fe81b558a4..7cce0bd6fc8 100644 --- a/packages/openapi3/test/tsp-openapi3/convert-openapi3-doc.test.ts +++ b/packages/openapi3/test/tsp-openapi3/convert-openapi3-doc.test.ts @@ -274,7 +274,7 @@ describe.each(versions)("convertOpenAPI3Document v%s", (version) => { "Expected literal response to use a generated component response model: " + tsp, ); strictEqual( - tsp.includes("@statusCode\n @minValue(400)\n @maxValue(499)"), + /@statusCode\s+@minValue\(400\)\s+@maxValue\(499\)/.test(tsp), true, "Expected range model to retain its status code: " + tsp, ); From 45ff937d07170948af1351d202030f983e84076a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 24 Aug 2026 15:01:44 +0000 Subject: [PATCH 12/14] fix(openapi3): preserve content type header name Co-authored-by: baywet <7905502+baywet@users.noreply.github.com> --- .../openapi3/src/cli/actions/convert/transforms/transforms.ts | 2 +- .../openapi3/test/tsp-openapi3/convert-openapi3-doc.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/openapi3/src/cli/actions/convert/transforms/transforms.ts b/packages/openapi3/src/cli/actions/convert/transforms/transforms.ts index b016dad979f..f32b397174a 100644 --- a/packages/openapi3/src/cli/actions/convert/transforms/transforms.ts +++ b/packages/openapi3/src/cli/actions/convert/transforms/transforms.ts @@ -415,7 +415,7 @@ function getResponseProperties( if (preferredContent && preferredContent[0] !== "application/json") { properties.push({ name: "contentType", - decorators: [{ name: "header", args: [] }], + decorators: [{ name: "header", args: ["Content-Type"] }], isOptional: false, schema: { type: "string", enum: [preferredContent[0]] }, }); diff --git a/packages/openapi3/test/tsp-openapi3/convert-openapi3-doc.test.ts b/packages/openapi3/test/tsp-openapi3/convert-openapi3-doc.test.ts index 7cce0bd6fc8..547b12caf4c 100644 --- a/packages/openapi3/test/tsp-openapi3/convert-openapi3-doc.test.ts +++ b/packages/openapi3/test/tsp-openapi3/convert-openapi3-doc.test.ts @@ -284,7 +284,7 @@ describe.each(versions)("convertOpenAPI3Document v%s", (version) => { "Expected default response model to be marked as an error: " + tsp, ); strictEqual( - tsp.includes('@header contentType: "application/xml";'), + tsp.includes('@header("Content-Type") contentType: "application/xml";'), true, "Expected component response models to retain the content type: " + tsp, ); From fff50d5b645687d10c2a0bce696b019044166ddf Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Mon, 24 Aug 2026 12:55:00 -0400 Subject: [PATCH 13/14] fix: compilation issue with status code Signed-off-by: Vincent Biret --- .../src/cli/actions/convert/utils/response-properties.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/openapi3/src/cli/actions/convert/utils/response-properties.ts b/packages/openapi3/src/cli/actions/convert/utils/response-properties.ts index 8a2a859da65..71d9f62b24f 100644 --- a/packages/openapi3/src/cli/actions/convert/utils/response-properties.ts +++ b/packages/openapi3/src/cli/actions/convert/utils/response-properties.ts @@ -6,18 +6,16 @@ import { getDecoratorsForSchema } from "./decorators.js"; export type LiteralStatusCode = `${number}`; export type WildcardStatusCode = "1XX" | "2XX" | "3XX" | "4XX" | "5XX"; -export type StatusCodes = LiteralStatusCode | WildcardStatusCode | "default"; +export type StatusCodes = LiteralStatusCode | WildcardStatusCode | "default" | (string & {}); -export function isValidLiteralStatusCode(statusCode: StatusCodes): boolean { +export function isValidLiteralStatusCode(statusCode: string): statusCode is LiteralStatusCode { if (statusCode === "default" || statusCode.endsWith("X")) return false; const literalStatusCode = parseInt(statusCode, 10); return isFinite(literalStatusCode) && literalStatusCode >= 100 && literalStatusCode <= 599; } -export function convertStatusCodeToProperty( - statusCode: Exclude, -): TypeSpecModelProperty { +export function convertStatusCodeToProperty(statusCode: string): TypeSpecModelProperty { const schema: OpenAPI3Schema = { type: "integer", format: "int32" }; if (statusCode === "1XX") { schema.minimum = 100; From d503c1c703a5bba9d21783bce3af47e216534820 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Thu, 27 Aug 2026 09:22:46 -0400 Subject: [PATCH 14/14] tests: updates outdated unit tests --- .../openapi3/test/tsp-openapi3/paths.test.ts | 46 +++++++++++++------ 1 file changed, 31 insertions(+), 15 deletions(-) diff --git a/packages/openapi3/test/tsp-openapi3/paths.test.ts b/packages/openapi3/test/tsp-openapi3/paths.test.ts index c48e93ddbf4..3afed26ee89 100644 --- a/packages/openapi3/test/tsp-openapi3/paths.test.ts +++ b/packages/openapi3/test/tsp-openapi3/paths.test.ts @@ -1221,7 +1221,15 @@ model Foo { message?: string; } - @route("/") @get op getFoo(): Foo; + @route("/") @get op getFoo(): Responses.TestResponse; + + namespace Responses { + /** test response */ + model TestResponse { + @statusCode statusCode: 200; + @body body: TestService.Foo; + } + } " `); @@ -1292,16 +1300,17 @@ model Foo { message?: string; } - @route("/") @get op getFoo(): GeneratedHelpers.DefaultResponse< - Description = "Overwritten description", - Body = Foo - >; + @route("/") @get op getFoo(): Responses.TestResponse; - @route("/") @head op headFoo(): GeneratedHelpers.DefaultResponse< - Description = "Base description", - Body = Foo - >; + @route("/") @head op headFoo(): Responses.TestResponse; + namespace Responses { + /** Base description */ + @error + model TestResponse { + @body body: TestService.Foo; + } + } namespace GeneratedHelpers { @doc(Description) @error @@ -1384,13 +1393,20 @@ model Foo { message?: string; } - @route("/") @get op getFoo(): { - /** my test header */ - @header("x-test") xTest?: string; + @route("/") @get op getFoo(): Responses.TestResponse; + + namespace Responses { + /** test response */ + model TestResponse { + @statusCode statusCode: 200; + + /** my test header */ + @header("x-test") xTest?: string; - @header("x-test2") xTest2?: string; - @body body: Foo; - }; + @header("x-test2") xTest2?: string; + @body body: TestService.Foo; + } + } " `);