diff --git a/.chronus/changes/openapi3-enum-style-annotated-2026-6-4-13-30-0.md b/.chronus/changes/openapi3-enum-style-annotated-2026-6-4-13-30-0.md
new file mode 100644
index 00000000000..3e7738fdddf
--- /dev/null
+++ b/.chronus/changes/openapi3-enum-style-annotated-2026-6-4-13-30-0.md
@@ -0,0 +1,42 @@
+---
+changeKind: feature
+packages:
+ - "@typespec/openapi3"
+---
+
+Add opt-in `enum-strategy` emitter option to emit TypeSpec enums as [annotated enumerations](https://spec.openapis.org/oas/v3.1.1.html#annotated-enumerations) (a `oneOf` of `const` subschemas with per-member `title`/`description`). Supported for OpenAPI 3.1.0 and above; emitting with OpenAPI 3.0.0 falls back to the default form and reports a warning.
+
+```yaml
+options:
+ "@typespec/openapi3":
+ enum-strategy: annotated
+```
+
+For example, the following TypeSpec:
+
+```typespec
+/** Type of pet. */
+enum PetType {
+ /** A loyal canine companion. */
+ @summary("Dog")
+ Dog: "dog",
+
+ /** A self-sufficient feline. */
+ @summary("Cat")
+ Cat: "cat",
+}
+```
+
+emits:
+
+```yaml
+PetType:
+ description: Type of pet.
+ oneOf:
+ - const: dog
+ title: Dog
+ description: A loyal canine companion.
+ - const: cat
+ title: Cat
+ description: A self-sufficient feline.
+```
diff --git a/packages/openapi3/README.md b/packages/openapi3/README.md
index d0ec89cf3e1..a9f822d64a7 100644
--- a/packages/openapi3/README.md
+++ b/packages/openapi3/README.md
@@ -172,6 +172,19 @@ See https://github.com/OAI/OpenAPI-Specification/discussions/4622 for discussion
| `kind` | `"parent-container" \| "fqn" \| "explicit-only"` | `"parent-container"` | Determines how to generate operation IDs when `@operationId` is not used.
Avaliable options are:
- `parent-container`: Uses the parent namespace and operation name to generate the ID.
- `fqn`: Uses the fully qualified name of the operation to generate the ID.
- `explicit-only`: Only use explicitly defined operation IDs. |
| `separator` | `string` | | Separator used to join segment in the operation name. |
+### `enum-strategy`
+
+**Type:** `"default" | "annotated"`
+
+**Default:** `"default"`
+
+How to emit TypeSpec enums. Options are:
+
+- `default`: Emit as a single schema using the `enum` keyword.
+- `annotated`: Emit as a `oneOf` of `const` subschemas annotated with `title` and `description`
+ from each member's `@summary` and `@doc`. Follows the OpenAPI 3.1.1 annotated enumerations pattern.
+ Only supported by OpenAPI 3.1.0 and above; on 3.0.0 the `default` style is used and a warning is reported.
+
## Decorators
### TypeSpec.OpenAPI
diff --git a/packages/openapi3/src/lib.ts b/packages/openapi3/src/lib.ts
index d5c29f3179b..48e6ef75d12 100644
--- a/packages/openapi3/src/lib.ts
+++ b/packages/openapi3/src/lib.ts
@@ -3,6 +3,7 @@ import { createTypeSpecLibrary, JSONSchemaType, paramMessage } from "@typespec/c
export type FileType = "yaml" | "json";
export type OpenAPIVersion = "3.0.0" | "3.1.0" | "3.2.0";
export type ExperimentalParameterExamplesStrategy = "data" | "serialized";
+export type EnumStrategy = "default" | "annotated";
export interface OpenAPI3EmitterOptions {
/**
* If the content should be serialized as YAML or JSON. Can be a single value or an array to emit multiple file types.
@@ -108,6 +109,20 @@ export interface OpenAPI3EmitterOptions {
/** Separator used to join segment in the operation name. */
separator?: string;
};
+
+ /**
+ * How to emit TypeSpec enums.
+ *
+ * - `default`: Emit as a single schema using the `enum` keyword.
+ * - `annotated`: Emit as a `oneOf` of `const` subschemas, each annotated with `title` and `description`
+ * when the corresponding enum member has `@summary` or `@doc`. This follows the OpenAPI 3.1.1
+ * [annotated enumerations](https://spec.openapis.org/oas/v3.1.1.html#annotated-enumerations) pattern.
+ * Only supported by OpenAPI 3.1.0 and above. When emitting OpenAPI 3.0.0, a warning will be reported
+ * and the `default` style will be used instead.
+ *
+ * @default "default"
+ */
+ "enum-strategy"?: EnumStrategy;
}
export type OperationIdStrategy = "parent-container" | "fqn" | "explicit-only";
@@ -268,6 +283,19 @@ const EmitterOptionsSchema: JSONSchemaType = {
},
],
} as any,
+ "enum-strategy": {
+ type: "string",
+ enum: ["default", "annotated"],
+ nullable: true,
+ default: "default",
+ description: [
+ "How to emit TypeSpec enums. Options are:",
+ " - `default`: Emit as a single schema using the `enum` keyword.",
+ " - `annotated`: Emit as a `oneOf` of `const` subschemas annotated with `title` and `description`",
+ " from each member's `@summary` and `@doc`. Follows the OpenAPI 3.1.1 annotated enumerations pattern.",
+ " Only supported by OpenAPI 3.1.0 and above; on 3.0.0 the `default` style is used and a warning is reported.",
+ ].join("\n"),
+ },
},
required: [],
};
@@ -428,6 +456,13 @@ export const $lib = createTypeSpecLibrary({
default: paramMessage`Default value is not supported in OpenAPI 3.0 ${"message"}`,
},
},
+ "enum-strategy-not-supported": {
+ severity: "warning",
+ messages: {
+ default:
+ "`enum-strategy: annotated` is only supported for OpenAPI 3.1.0 and above. The default enum strategy will be used for OpenAPI 3.0.0.",
+ },
+ },
},
emitter: {
options: EmitterOptionsSchema as JSONSchemaType,
diff --git a/packages/openapi3/src/openapi.ts b/packages/openapi3/src/openapi.ts
index 0c41f87d160..ad085c74e47 100644
--- a/packages/openapi3/src/openapi.ts
+++ b/packages/openapi3/src/openapi.ts
@@ -33,6 +33,7 @@ import {
Namespace,
navigateTypesInNamespace,
NewLine,
+ NoTarget,
Program,
resolvePath,
Service,
@@ -86,6 +87,7 @@ import { getExampleOrExamples, OperationExamples, resolveOperationExamples } fro
import { JsonSchemaModule, resolveJsonSchemaModule } from "./json-schema.js";
import {
createDiagnostic,
+ EnumStrategy,
FileType,
OpenAPI3EmitterOptions,
OpenAPIVersion,
@@ -219,6 +221,11 @@ export function resolveOptions(
const openapiVersions = resolvedOptions["openapi-versions"] ?? ["3.0.0"];
+ const enumStrategy: EnumStrategy = resolvedOptions["enum-strategy"] ?? "default";
+ if (enumStrategy === "annotated" && openapiVersions.includes("3.0.0")) {
+ reportDiagnostic(context.program, { code: "enum-strategy-not-supported", target: NoTarget });
+ }
+
const specDir = openapiVersions.length > 1 ? "{openapi-version}" : "";
return {
fileTypes,
@@ -231,6 +238,7 @@ export function resolveOptions(
sealObjectSchemas: resolvedOptions["seal-object-schemas"],
parameterExamplesStrategy: resolvedOptions["experimental-parameter-examples"],
operationIdStrategy: resolveOperationIdStrategy(resolvedOptions["operation-id-strategy"]),
+ enumStrategy,
};
}
@@ -272,6 +280,7 @@ export interface ResolvedOpenAPI3EmitterOptions {
sealObjectSchemas: boolean;
parameterExamplesStrategy?: "data" | "serialized";
operationIdStrategy: { kind: OperationIdStrategy; separator: string };
+ enumStrategy: EnumStrategy;
}
function createOAPIEmitter(
diff --git a/packages/openapi3/src/schema-emitter-3-1.ts b/packages/openapi3/src/schema-emitter-3-1.ts
index 42b07d00c28..af58ad561d2 100644
--- a/packages/openapi3/src/schema-emitter-3-1.ts
+++ b/packages/openapi3/src/schema-emitter-3-1.ts
@@ -12,9 +12,11 @@ import {
compilerAssert,
Enum,
getDiscriminatedUnion,
+ getDoc,
getExamples,
getMaxValueExclusive,
getMinValueExclusive,
+ getSummary,
IntrinsicScalarName,
IntrinsicType,
Model,
@@ -183,6 +185,10 @@ export class OpenAPI31SchemaEmitter extends OpenAPI3SchemaEmitterBase();
const enumValues = new Set();
for (const member of en.members.values()) {
@@ -200,6 +206,26 @@ export class OpenAPI31SchemaEmitter extends OpenAPI3SchemaEmitterBase {
const program = this.emitter.getProgram();
const [discriminated] = getDiscriminatedUnion(program, union);
diff --git a/packages/openapi3/test/enums.test.ts b/packages/openapi3/test/enums.test.ts
index 3d8a325a441..087ba49ef12 100644
--- a/packages/openapi3/test/enums.test.ts
+++ b/packages/openapi3/test/enums.test.ts
@@ -1,6 +1,7 @@
import { expectDiagnostics } from "@typespec/compiler/testing";
import { deepStrictEqual, strictEqual } from "assert";
import { it } from "vitest";
+import { diagnoseOpenApiFor } from "./test-host.js";
import { supportedVersions, worksFor } from "./works-for.js";
worksFor(supportedVersions, ({ diagnoseOpenApiFor, oapiForModel }) => {
@@ -48,3 +49,190 @@ worksFor(["3.1.0"], ({ oapiForModel }) => {
});
});
});
+
+worksFor(["3.1.0", "3.2.0"], ({ oapiForModel }) => {
+ it("emits annotated enums with `enum-strategy: annotated`", async () => {
+ const res = await oapiForModel(
+ "PetType",
+ `
+ @doc("Type of pet.")
+ enum PetType {
+ /** A loyal canine companion. */
+ @summary("Dog")
+ Dog: "dog",
+
+ /** A self-sufficient feline. */
+ @summary("Cat")
+ Cat: "cat",
+ }
+ `,
+ { "enum-strategy": "annotated" },
+ );
+
+ deepStrictEqual(res.schemas.PetType, {
+ description: "Type of pet.",
+ oneOf: [
+ {
+ const: "dog",
+ title: "Dog",
+ description: "A loyal canine companion.",
+ },
+ {
+ const: "cat",
+ title: "Cat",
+ description: "A self-sufficient feline.",
+ },
+ ],
+ });
+ });
+
+ it("emits annotated enums for number-valued enums", async () => {
+ const res = await oapiForModel(
+ "Priority",
+ `
+ enum Priority {
+ /** Low priority. */
+ Low: 1,
+ /** High priority. */
+ High: 10,
+ }
+ `,
+ { "enum-strategy": "annotated" },
+ );
+
+ deepStrictEqual(res.schemas.Priority, {
+ oneOf: [
+ { const: 1, description: "Low priority." },
+ { const: 10, description: "High priority." },
+ ],
+ });
+ });
+
+ it("emits annotated enums for mixed-type enums", async () => {
+ const res = await oapiForModel(
+ "Mixed",
+ `
+ enum Mixed {
+ asString: "value",
+ asNumber: 42,
+ }
+ `,
+ { "enum-strategy": "annotated" },
+ );
+
+ deepStrictEqual(res.schemas.Mixed, {
+ oneOf: [{ const: "value" }, { const: 42 }],
+ });
+ });
+
+ it("emits annotated enums omitting title/description for members without docs", async () => {
+ const res = await oapiForModel(
+ "Color",
+ `
+ enum Color {
+ Red,
+ Green: "green",
+ /** Blue is the warmest color. */
+ Blue: "blue",
+ }
+ `,
+ { "enum-strategy": "annotated" },
+ );
+
+ deepStrictEqual(res.schemas.Color, {
+ oneOf: [
+ { const: "Red" },
+ { const: "green" },
+ { const: "blue", description: "Blue is the warmest color." },
+ ],
+ });
+ });
+
+ it("emits default enum form when `enum-strategy` is unset", async () => {
+ const res = await oapiForModel(
+ "PetType",
+ `
+ /** Type of pet. */
+ enum PetType {
+ /** A loyal canine companion. */
+ @summary("Dog")
+ Dog: "dog",
+ }
+ `,
+ );
+
+ deepStrictEqual(res.schemas.PetType, {
+ type: "string",
+ enum: ["dog"],
+ description: "Type of pet.",
+ });
+ });
+});
+
+worksFor(["3.0.0"], ({ emitOpenApiWithDiagnostics }) => {
+ it("falls back to the default enum form when `enum-strategy: annotated` is set", async () => {
+ const [doc, diagnostics] = await emitOpenApiWithDiagnostics(
+ `
+ @service
+ namespace Test;
+
+ /** Type of pet. */
+ enum PetType {
+ /** A loyal canine companion. */
+ Dog: "dog",
+ /** A self-sufficient feline. */
+ Cat: "cat",
+ }
+ op read(): PetType;
+ `,
+ { "enum-strategy": "annotated" },
+ );
+
+ expectDiagnostics(diagnostics, {
+ code: "@typespec/openapi3/enum-strategy-not-supported",
+ severity: "warning",
+ });
+
+ deepStrictEqual(doc.components!.schemas!.PetType, {
+ type: "string",
+ enum: ["dog", "cat"],
+ description: "Type of pet.",
+ });
+ });
+});
+
+it("warns when `enum-strategy: annotated` is used with OpenAPI 3.0.0", async () => {
+ const diagnostics = await diagnoseOpenApiFor(`enum PetType { Dog: "dog" }`, {
+ "enum-strategy": "annotated",
+ "openapi-versions": ["3.0.0"],
+ });
+
+ expectDiagnostics(diagnostics, {
+ code: "@typespec/openapi3/enum-strategy-not-supported",
+ severity: "warning",
+ });
+});
+
+it("warns once when `enum-strategy: annotated` is used with mixed openapi-versions including 3.0.0", async () => {
+ const diagnostics = await diagnoseOpenApiFor(`enum PetType { Dog: "dog" }`, {
+ "enum-strategy": "annotated",
+ "openapi-versions": ["3.0.0", "3.1.0"],
+ });
+
+ expectDiagnostics(diagnostics, {
+ code: "@typespec/openapi3/enum-strategy-not-supported",
+ severity: "warning",
+ });
+});
+
+it("does not warn when `enum-strategy: annotated` is used with only 3.1.0/3.2.0", async () => {
+ const diagnostics = await diagnoseOpenApiFor(`enum PetType { Dog: "dog" }`, {
+ "enum-strategy": "annotated",
+ "openapi-versions": ["3.1.0", "3.2.0"],
+ });
+
+ strictEqual(
+ diagnostics.filter((d) => d.code === "@typespec/openapi3/enum-strategy-not-supported").length,
+ 0,
+ );
+});
diff --git a/website/src/content/docs/docs/emitters/openapi3/reference/emitter.md b/website/src/content/docs/docs/emitters/openapi3/reference/emitter.md
index f1c6d3539bf..39f7fc1e342 100644
--- a/website/src/content/docs/docs/emitters/openapi3/reference/emitter.md
+++ b/website/src/content/docs/docs/emitters/openapi3/reference/emitter.md
@@ -165,3 +165,16 @@ See https://github.com/OAI/OpenAPI-Specification/discussions/4622 for discussion
| ----------- | ------------------------------------------------ | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `kind` | `"parent-container" \| "fqn" \| "explicit-only"` | `"parent-container"` | Determines how to generate operation IDs when `@operationId` is not used.
Avaliable options are:
- `parent-container`: Uses the parent namespace and operation name to generate the ID.
- `fqn`: Uses the fully qualified name of the operation to generate the ID.
- `explicit-only`: Only use explicitly defined operation IDs. |
| `separator` | `string` | | Separator used to join segment in the operation name. |
+
+### `enum-strategy`
+
+**Type:** `"default" | "annotated"`
+
+**Default:** `"default"`
+
+How to emit TypeSpec enums. Options are:
+
+- `default`: Emit as a single schema using the `enum` keyword.
+- `annotated`: Emit as a `oneOf` of `const` subschemas annotated with `title` and `description`
+ from each member's `@summary` and `@doc`. Follows the OpenAPI 3.1.1 annotated enumerations pattern.
+ Only supported by OpenAPI 3.1.0 and above; on 3.0.0 the `default` style is used and a warning is reported.