Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions .chronus/changes/openapi3-enum-style-annotated-2026-6-4-13-30-0.md
Original file line number Diff line number Diff line change
@@ -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.
```
10 changes: 5 additions & 5 deletions .github/aw/actions-lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
"entries": {
"actions/github-script@v9": {
"repo": "actions/github-script",
"version": "v9.0.0",
"sha": "3a2844b7e9c422d3c10d287c895573f7108da1b3"
"version": "v9",
"sha": "373c709c69115d41ff229c7e5df9f8788daa9553"
},
"github/gh-aw-actions/setup@v0.71.5": {
"github/gh-aw-actions/setup@v0.68.3": {
"repo": "github/gh-aw-actions/setup",
"version": "v0.71.5",
"sha": "b8068426813005612b960b5ab0b8bd2c27142323"
"version": "v0.68.3",
"sha": "ba90f2186d7ad780ec640f364005fa24e797b360"
},
"github/gh-aw/actions/setup@v0.57.2": {
"repo": "github/gh-aw/actions/setup",
Expand Down
358 changes: 133 additions & 225 deletions .github/workflows/issue-triage.lock.yml

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions packages/openapi3/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,17 @@ See https://github.com/OAI/OpenAPI-Specification/discussions/4622 for discussion

**Type:** `undefined`

### `enum-strategy`

**Type:** `"default" | "annotated"`

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
Expand Down
35 changes: 35 additions & 0 deletions packages/openapi3/src/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -268,6 +283,19 @@ const EmitterOptionsSchema: JSONSchemaType<OpenAPI3EmitterOptions> = {
},
],
} 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: [],
};
Expand Down Expand Up @@ -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<OpenAPI3EmitterOptions>,
Expand Down
9 changes: 9 additions & 0 deletions packages/openapi3/src/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
Namespace,
navigateTypesInNamespace,
NewLine,
NoTarget,
Program,
resolvePath,
Service,
Expand Down Expand Up @@ -86,6 +87,7 @@ import { getExampleOrExamples, OperationExamples, resolveOperationExamples } fro
import { JsonSchemaModule, resolveJsonSchemaModule } from "./json-schema.js";
import {
createDiagnostic,
EnumStrategy,
FileType,
OpenAPI3EmitterOptions,
OpenAPIVersion,
Expand Down Expand Up @@ -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,
Expand All @@ -231,6 +238,7 @@ export function resolveOptions(
sealObjectSchemas: resolvedOptions["seal-object-schemas"],
parameterExamplesStrategy: resolvedOptions["experimental-parameter-examples"],
operationIdStrategy: resolveOperationIdStrategy(resolvedOptions["operation-id-strategy"]),
enumStrategy,
};
}

Expand Down Expand Up @@ -272,6 +280,7 @@ export interface ResolvedOpenAPI3EmitterOptions {
sealObjectSchemas: boolean;
parameterExamplesStrategy?: "data" | "serialized";
operationIdStrategy: { kind: OperationIdStrategy; separator: string };
enumStrategy: EnumStrategy;
}

function createOAPIEmitter(
Expand Down
26 changes: 26 additions & 0 deletions packages/openapi3/src/schema-emitter-3-1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ import {
compilerAssert,
Enum,
getDiscriminatedUnion,
getDoc,
getExamples,
getMaxValueExclusive,
getMinValueExclusive,
getSummary,
IntrinsicScalarName,
IntrinsicType,
Model,
Expand Down Expand Up @@ -183,6 +185,10 @@ export class OpenAPI31SchemaEmitter extends OpenAPI3SchemaEmitterBase<OpenAPISch
return {};
}

if (this._options.enumStrategy === "annotated") {
return this.#annotatedEnumSchema(en);
}

const enumTypes = new Set<JsonType>();
const enumValues = new Set<string | number>();
for (const member of en.members.values()) {
Expand All @@ -200,6 +206,26 @@ export class OpenAPI31SchemaEmitter extends OpenAPI3SchemaEmitterBase<OpenAPISch
return this.applyConstraints(en, schema);
}

#annotatedEnumSchema(en: Enum): OpenAPISchema3_1 {
const program = this.emitter.getProgram();
const oneOf: OpenAPISchema3_1[] = [];
for (const member of en.members.values()) {
const value = member.value ?? member.name;
const subschema: OpenAPISchema3_1 = { const: value };
const title = getSummary(program, member);
if (title !== undefined) {
subschema.title = title;
}
const description = getDoc(program, member);
if (description !== undefined) {
subschema.description = description;
}
oneOf.push(subschema);
}

return this.applyConstraints(en, { oneOf });
}

unionSchema(union: Union): ObjectBuilder<OpenAPISchema3_1> {
const program = this.emitter.getProgram();
const [discriminated] = getDiscriminatedUnion(program, union);
Expand Down
Loading