From dad11eb8eb7f3d0f9f65f889edfce5c3686b459d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 22 May 2026 13:37:10 +0000 Subject: [PATCH 1/4] Initial plan From fce92aae827cd13bb182833457eb4ce5b2716164 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 22 May 2026 13:55:51 +0000 Subject: [PATCH 2/4] Add summary and kind fields to @tagMetadata decorator - Add summary and kind to TagMetadata model in decorators.tsp - Update generated-defs TypeSpec.OpenAPI.ts with new fields - Emit summary/kind as native fields for OpenAPI 3.2 - Emit summary/kind as x-oai-summary/x-oai-kind extensions for OpenAPI 3.0/3.1 - Update converter to import summary/kind from both native and x-oai- fields - Add tests for version-specific summary/kind behavior - Update tag-metadata spec snapshot to include summary and kind - Add 3.2 tag-metadata spec snapshot test - Add changelog entry Agent-Logs-Url: https://github.com/microsoft/typespec/sessions/73460656-985a-4d04-9a50-f6010a0c0987 Co-authored-by: baywet <7905502+baywet@users.noreply.github.com> --- ...etadata-summary-kind-2026-5-22-13-53-44.md | 14 +++ .../generated-defs/TypeSpec.OpenAPI.ts | 2 + packages/openapi/lib/decorators.tsp | 6 ++ .../convert/generators/generate-tags.ts | 6 +- .../src/cli/actions/convert/interfaces.ts | 2 + .../convert/transforms/transform-tags.ts | 33 +++--- packages/openapi3/src/openapi.ts | 11 ++ packages/openapi3/test/tagmetadata.test.ts | 101 ++++++++++++++++++ .../output/tag-metadata-3-2/main.tsp | 27 +++++ .../tsp-openapi3/output/tag-metadata/main.tsp | 9 +- .../specs/tag-metadata-3-2/service.yml | 33 ++++++ .../specs/tag-metadata/service.yml | 2 + 12 files changed, 231 insertions(+), 15 deletions(-) create mode 100644 .chronus/changes/add-tag-metadata-summary-kind-2026-5-22-13-53-44.md create mode 100644 packages/openapi3/test/tsp-openapi3/output/tag-metadata-3-2/main.tsp create mode 100644 packages/openapi3/test/tsp-openapi3/specs/tag-metadata-3-2/service.yml diff --git a/.chronus/changes/add-tag-metadata-summary-kind-2026-5-22-13-53-44.md b/.chronus/changes/add-tag-metadata-summary-kind-2026-5-22-13-53-44.md new file mode 100644 index 00000000000..453708fb781 --- /dev/null +++ b/.chronus/changes/add-tag-metadata-summary-kind-2026-5-22-13-53-44.md @@ -0,0 +1,14 @@ +--- +changeKind: feature +packages: + - "@typespec/openapi" + - "@typespec/openapi3" +--- + +Add `summary` and `kind` fields to `@tagMetadata` decorator. + +For OpenAPI 3.2, these fields are emitted as native tag object fields. For OpenAPI 3.0/3.1, they are emitted as `x-oai-summary` and `x-oai-kind` extensions. The OpenAPI converter also supports importing `x-oai-summary`, `x-oai-kind` (from 3.0/3.1) and native `summary`, `kind` (from 3.2) back to TypeSpec. + +```typespec +@tagMetadata("foo", #{ summary: "all operations that allow doing Foo", kind: "FooGroup" }) +``` diff --git a/packages/openapi/generated-defs/TypeSpec.OpenAPI.ts b/packages/openapi/generated-defs/TypeSpec.OpenAPI.ts index 48b894737b3..93e555b29df 100644 --- a/packages/openapi/generated-defs/TypeSpec.OpenAPI.ts +++ b/packages/openapi/generated-defs/TypeSpec.OpenAPI.ts @@ -22,6 +22,8 @@ export interface TagMetadata { readonly description?: string; readonly externalDocs?: ExternalDocs; readonly parent?: string; + readonly summary?: string; + readonly kind?: string; } export interface Contact { diff --git a/packages/openapi/lib/decorators.tsp b/packages/openapi/lib/decorators.tsp index e00bf2d1126..547707cec51 100644 --- a/packages/openapi/lib/decorators.tsp +++ b/packages/openapi/lib/decorators.tsp @@ -127,6 +127,12 @@ model TagMetadata { /** The name of a tag that this tag is nested under. Only supported in OpenAPI 3.2. For 3.0 and 3.1, this will be converted to `x-parent`. */ parent?: string; + /** A short summary of the tag, used for display purposes. Only supported natively in OpenAPI 3.2. For 3.0 and 3.1, this will be emitted as `x-oai-summary`. */ + summary?: string; + + /** A machine-readable string to categorize what sort of tag it is. Any string value can be used. Only supported natively in OpenAPI 3.2. For 3.0 and 3.1, this will be emitted as `x-oai-kind`. */ + kind?: string; + /** Attach some custom data, The extension key must start with `x-`. */ ...Record; } diff --git a/packages/openapi3/src/cli/actions/convert/generators/generate-tags.ts b/packages/openapi3/src/cli/actions/convert/generators/generate-tags.ts index b759356a167..1080d3c7e36 100644 --- a/packages/openapi3/src/cli/actions/convert/generators/generate-tags.ts +++ b/packages/openapi3/src/cli/actions/convert/generators/generate-tags.ts @@ -21,9 +21,11 @@ export function generateTags(tags: TypeSpecTagMetadata[]): string { const definitions = tags.map((tag) => { const description = tag.description ? `description: "${tag.description}"` : ""; const externalDocs = generateExternalDocs(tag.externalDocs); + const summary = tag.summary ? `summary: "${tag.summary}"` : ""; + const kind = tag.kind ? `kind: "${tag.kind}"` : ""; const tagMetadata = - description || externalDocs - ? `, #{${[description, externalDocs].filter((x) => !!x).join(", ")}}` + description || externalDocs || summary || kind + ? `, #{${[description, externalDocs, summary, kind].filter((x) => !!x).join(", ")}}` : ""; return `@tagMetadata("${tag.name}"${tagMetadata})`; }); diff --git a/packages/openapi3/src/cli/actions/convert/interfaces.ts b/packages/openapi3/src/cli/actions/convert/interfaces.ts index 82efbe8db62..c1decacbe54 100644 --- a/packages/openapi3/src/cli/actions/convert/interfaces.ts +++ b/packages/openapi3/src/cli/actions/convert/interfaces.ts @@ -32,6 +32,8 @@ export interface TypeSpecTagMetadata { name: string; description?: string; externalDocs?: TypeSpecExternalDocs; + summary?: string; + kind?: string; } export interface TypeSpecExternalDocs { diff --git a/packages/openapi3/src/cli/actions/convert/transforms/transform-tags.ts b/packages/openapi3/src/cli/actions/convert/transforms/transform-tags.ts index 84de81cbd98..99ae275c8f7 100644 --- a/packages/openapi3/src/cli/actions/convert/transforms/transform-tags.ts +++ b/packages/openapi3/src/cli/actions/convert/transforms/transform-tags.ts @@ -1,16 +1,25 @@ -import { OpenAPI3Tag } from "../../../../types.js"; +import { OpenAPI3Tag, OpenAPITag3_2 } from "../../../../types.js"; import { TypeSpecTagMetadata } from "../interfaces.js"; export function transformTags(tags: OpenAPI3Tag[]): TypeSpecTagMetadata[] { - return tags.map((tag) => ({ - name: tag.name, - description: tag.description, - externalDocs: - tag.externalDocs?.url || tag.externalDocs?.description - ? { - url: tag.externalDocs.url, - description: tag.externalDocs.description, - } - : undefined, - })); + return tags.map((tag) => { + const tag32 = tag as OpenAPITag3_2; + // Support both native 3.2 fields and x-oai- prefixed extensions for 3.0/3.1 + const summary = tag32.summary ?? (tag as any)["x-oai-summary"]; + const kind = tag32.kind ?? (tag as any)["x-oai-kind"]; + + return { + name: tag.name, + description: tag.description, + externalDocs: + tag.externalDocs?.url || tag.externalDocs?.description + ? { + url: tag.externalDocs.url, + description: tag.externalDocs.description, + } + : undefined, + summary, + kind, + }; + }); } diff --git a/packages/openapi3/src/openapi.ts b/packages/openapi3/src/openapi.ts index 8425d7ad001..9c67c48b867 100644 --- a/packages/openapi3/src/openapi.ts +++ b/packages/openapi3/src/openapi.ts @@ -1831,6 +1831,17 @@ function createOAPIEmitter( if (specVersion !== "3.2.0" && tag.parent) { delete (tagData as { parent?: string }).parent; } + // For OpenAPI 3.0 and 3.1, convert 'summary' and 'kind' to x-oai- prefixed extensions + if (specVersion !== "3.2.0") { + if (tag.summary) { + (tagData as unknown as Record)["x-oai-summary"] = tag.summary; + delete (tagData as { summary?: string }).summary; + } + if (tag.kind) { + (tagData as unknown as Record)["x-oai-kind"] = tag.kind; + delete (tagData as { kind?: string }).kind; + } + } tags.push(tagData); } diff --git a/packages/openapi3/test/tagmetadata.test.ts b/packages/openapi3/test/tagmetadata.test.ts index 946a0d5a744..5dcf6417be2 100644 --- a/packages/openapi3/test/tagmetadata.test.ts +++ b/packages/openapi3/test/tagmetadata.test.ts @@ -226,3 +226,104 @@ describe("tag metadata with parent field", () => { ]); }); }); + +// Tests for summary and kind fields - version specific behavior +describe("tag metadata with summary and kind fields", () => { + it("OpenAPI 3.2 should emit summary and kind as native fields", async () => { + const res = await OpenAPISpecHelpers["3.2.0"].openApiFor( + ` + @service + @tagMetadata("foo", #{summary: "all operations that allow doing Foo", kind: "FooGroup"}) + namespace PetStore { + @tag("foo") op test(): string; + } + `, + ); + + deepStrictEqual(res.tags, [ + { + name: "foo", + summary: "all operations that allow doing Foo", + kind: "FooGroup", + }, + ]); + }); + + it("OpenAPI 3.1 should emit summary as x-oai-summary and kind as x-oai-kind", async () => { + const res = await OpenAPISpecHelpers["3.1.0"].openApiFor( + ` + @service + @tagMetadata("foo", #{summary: "all operations that allow doing Foo", kind: "FooGroup"}) + namespace PetStore { + @tag("foo") op test(): string; + } + `, + ); + + deepStrictEqual(res.tags, [ + { + name: "foo", + "x-oai-summary": "all operations that allow doing Foo", + "x-oai-kind": "FooGroup", + }, + ]); + }); + + it("OpenAPI 3.0 should emit summary as x-oai-summary and kind as x-oai-kind", async () => { + const res = await OpenAPISpecHelpers["3.0.0"].openApiFor( + ` + @service + @tagMetadata("foo", #{summary: "all operations that allow doing Foo", kind: "FooGroup"}) + namespace PetStore { + @tag("foo") op test(): string; + } + `, + ); + + deepStrictEqual(res.tags, [ + { + name: "foo", + "x-oai-summary": "all operations that allow doing Foo", + "x-oai-kind": "FooGroup", + }, + ]); + }); + + it("OpenAPI 3.2 should emit summary only if kind is absent", async () => { + const res = await OpenAPISpecHelpers["3.2.0"].openApiFor( + ` + @service + @tagMetadata("foo", #{summary: "all operations that allow doing Foo"}) + namespace PetStore { + @tag("foo") op test(): string; + } + `, + ); + + deepStrictEqual(res.tags, [ + { + name: "foo", + summary: "all operations that allow doing Foo", + }, + ]); + }); + + it("OpenAPI 3.2 should emit kind only if summary is absent", async () => { + const res = await OpenAPISpecHelpers["3.2.0"].openApiFor( + ` + @service + @tagMetadata("foo", #{kind: "FooGroup"}) + namespace PetStore { + @tag("foo") op test(): string; + } + `, + ); + + deepStrictEqual(res.tags, [ + { + name: "foo", + kind: "FooGroup", + }, + ]); + }); +}); diff --git a/packages/openapi3/test/tsp-openapi3/output/tag-metadata-3-2/main.tsp b/packages/openapi3/test/tsp-openapi3/output/tag-metadata-3-2/main.tsp new file mode 100644 index 00000000000..3c3fcb700c2 --- /dev/null +++ b/packages/openapi3/test/tsp-openapi3/output/tag-metadata-3-2/main.tsp @@ -0,0 +1,27 @@ +import "@typespec/http"; +import "@typespec/openapi"; +import "@typespec/openapi3"; + +using Http; +using OpenAPI; + +@service(#{ title: "(title)" }) +@info(#{ version: "0.0.0" }) +@tagMetadata( + "extensive", + #{ + description: "An extensive operation", + summary: "A summary of the extensive tag", + kind: "OperationGroup", + } +) +namespace title; + +model Pet { + name: string; +} + +@tag("extensive") +@route("/") +@get +op extensive(): Pet; diff --git a/packages/openapi3/test/tsp-openapi3/output/tag-metadata/main.tsp b/packages/openapi3/test/tsp-openapi3/output/tag-metadata/main.tsp index 18d5ad53cee..94a0a1c8499 100644 --- a/packages/openapi3/test/tsp-openapi3/output/tag-metadata/main.tsp +++ b/packages/openapi3/test/tsp-openapi3/output/tag-metadata/main.tsp @@ -7,7 +7,14 @@ using OpenAPI; @service(#{ title: "(title)" }) @info(#{ version: "0.0.0" }) -@tagMetadata("extensive", #{ description: "An extensive operation" }) +@tagMetadata( + "extensive", + #{ + description: "An extensive operation", + summary: "A summary of the extensive tag", + kind: "OperationGroup", + } +) namespace title; model Pet { diff --git a/packages/openapi3/test/tsp-openapi3/specs/tag-metadata-3-2/service.yml b/packages/openapi3/test/tsp-openapi3/specs/tag-metadata-3-2/service.yml new file mode 100644 index 00000000000..7ce1af0c390 --- /dev/null +++ b/packages/openapi3/test/tsp-openapi3/specs/tag-metadata-3-2/service.yml @@ -0,0 +1,33 @@ +openapi: 3.2.0 +info: + title: (title) + version: 0.0.0 +tags: + - name: extensive + description: An extensive operation + summary: A summary of the extensive tag + kind: OperationGroup + parent: root +paths: + /: + get: + operationId: extensive + parameters: [] + responses: + "200": + description: The request has succeeded. + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + tags: + - extensive +components: + schemas: + Pet: + type: object + required: + - name + properties: + name: + type: string diff --git a/packages/openapi3/test/tsp-openapi3/specs/tag-metadata/service.yml b/packages/openapi3/test/tsp-openapi3/specs/tag-metadata/service.yml index 1e89668fa1b..39883beea4a 100644 --- a/packages/openapi3/test/tsp-openapi3/specs/tag-metadata/service.yml +++ b/packages/openapi3/test/tsp-openapi3/specs/tag-metadata/service.yml @@ -5,6 +5,8 @@ info: tags: - name: extensive description: An extensive operation + x-oai-summary: A summary of the extensive tag + x-oai-kind: OperationGroup paths: /: get: From 2e7c91ed4ff057d78937cdf463031db07e7eae36 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 22 May 2026 13:59:55 +0000 Subject: [PATCH 3/4] Improve type safety in transform-tags.ts: use Extension keys instead of any cast Agent-Logs-Url: https://github.com/microsoft/typespec/sessions/73460656-985a-4d04-9a50-f6010a0c0987 Co-authored-by: baywet <7905502+baywet@users.noreply.github.com> --- .../src/cli/actions/convert/transforms/transform-tags.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/openapi3/src/cli/actions/convert/transforms/transform-tags.ts b/packages/openapi3/src/cli/actions/convert/transforms/transform-tags.ts index 99ae275c8f7..0550790d009 100644 --- a/packages/openapi3/src/cli/actions/convert/transforms/transform-tags.ts +++ b/packages/openapi3/src/cli/actions/convert/transforms/transform-tags.ts @@ -5,8 +5,9 @@ export function transformTags(tags: OpenAPI3Tag[]): TypeSpecTagMetadata[] { return tags.map((tag) => { const tag32 = tag as OpenAPITag3_2; // Support both native 3.2 fields and x-oai- prefixed extensions for 3.0/3.1 - const summary = tag32.summary ?? (tag as any)["x-oai-summary"]; - const kind = tag32.kind ?? (tag as any)["x-oai-kind"]; + const summary: string | undefined = + tag32.summary ?? (tag["x-oai-summary"] as string | undefined); + const kind: string | undefined = tag32.kind ?? (tag["x-oai-kind"] as string | undefined); return { name: tag.name, From 4e636c5522c54642cd794431ae9d6b91ad0c5510 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 26 May 2026 18:38:11 +0000 Subject: [PATCH 4/4] docs: regenerate openapi reference documentation with summary and kind fields Co-authored-by: baywet <7905502+baywet@users.noreply.github.com> --- .../docs/libraries/openapi/reference/data-types.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/website/src/content/docs/docs/libraries/openapi/reference/data-types.md b/website/src/content/docs/docs/libraries/openapi/reference/data-types.md index b6e89310e0c..2595d35015c 100644 --- a/website/src/content/docs/docs/libraries/openapi/reference/data-types.md +++ b/website/src/content/docs/docs/libraries/openapi/reference/data-types.md @@ -85,9 +85,11 @@ model TypeSpec.OpenAPI.TagMetadata #### Properties -| Name | Type | Description | -| ------------- | --------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | -| description? | `string` | A description of the API. | -| externalDocs? | [`ExternalDocs`](./data-types.md#TypeSpec.OpenAPI.ExternalDocs) | An external Docs information of the API. | -| parent? | `string` | The name of a tag that this tag is nested under. Only supported in OpenAPI 3.2. For 3.0 and 3.1, this will be converted to `x-parent`. | -| | `unknown` | Additional properties | +| Name | Type | Description | +| ------------- | --------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| description? | `string` | A description of the API. | +| externalDocs? | [`ExternalDocs`](./data-types.md#TypeSpec.OpenAPI.ExternalDocs) | An external Docs information of the API. | +| parent? | `string` | The name of a tag that this tag is nested under. Only supported in OpenAPI 3.2. For 3.0 and 3.1, this will be converted to `x-parent`. | +| summary? | `string` | A short summary of the tag, used for display purposes. Only supported natively in OpenAPI 3.2. For 3.0 and 3.1, this will be emitted as `x-oai-summary`. | +| kind? | `string` | A machine-readable string to categorize what sort of tag it is. Any string value can be used. Only supported natively in OpenAPI 3.2. For 3.0 and 3.1, this will be emitted as `x-oai-kind`. | +| | `unknown` | Additional properties |