Skip to content

Commit 2eb4bd0

Browse files
feat(typespec-autorest): add type-name-strategy option to remove namespace prefix from names (#4660)
Fixes #2055 ## Problem `@azure-tools/typespec-autorest` prepends the namespace to model names when generating Swagger definition names (e.g. `LiftrBase.MarketplaceDetails`). Client emitters (C#, PowerShell, etc.) ignore the namespace and use just the type name (`MarketplaceDetails`). This inconsistency causes breaking changes when migrating from swagger-based codegen to TypeSpec-native codegen. ## Change Adds a new emitter option `type-name-strategy`: - `"namespaced"` (default): current behavior — names include the namespace prefix for types outside the service namespace (e.g. `LiftrBase.Foo`). The service (and root `TypeSpec`) namespace is always stripped. - `"name-only"`: names use only the type name without any namespace prefix (e.g. `Foo`), matching what client emitters produce. ```yaml options: "@azure-tools/typespec-autorest": type-name-strategy: "name-only" ``` The strategy applies globally to all OpenAPI names derived from TypeSpec types (definition/schema names, parameter keys, inline names, `x-typespec-name`) via the shared `namespaceFilter`, keeping everything consistent. When two types from different namespaces collapse to the same name, the existing `@typespec/openapi/duplicate-type-name` error is reported (no auto fallback to the qualified name). `@clientName` and `@friendlyName` continue to take precedence. ## Backward compatibility Default is `"namespaced"`, so existing behavior is unchanged unless the option is explicitly set to `"name-only"`. ## Tests Added `packages/typespec-autorest/test/type-name-strategy.test.ts` covering: - default / explicit `namespaced` keeps the prefix, and same-named types in different namespaces coexist - `name-only` removes the prefix from definitions - `name-only` collision across namespaces errors with `duplicate-type-name` - `@friendlyName` still wins under `name-only`
1 parent d05116c commit 2eb4bd0

7 files changed

Lines changed: 152 additions & 0 deletions

File tree

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
changeKind: feature
3+
packages:
4+
- "@azure-tools/typespec-autorest"
5+
---
6+
7+
Add `type-name-strategy` emitter option to control how OpenAPI names are derived from TypeSpec types. The new `"name-only"` strategy removes the namespace prefix from names (e.g. `Foo` instead of `LiftrBase.Foo`), matching the names used by client emitters. The default `"namespaced"` keeps the current behavior. When two types collapse to the same name, a `duplicate-type-name` error is reported.
8+
9+
```yaml
10+
options:
11+
"@azure-tools/typespec-autorest":
12+
type-name-strategy: "name-only"
13+
```

packages/typespec-autorest/README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,14 @@ Determines whether output should be split into multiple files. The only supporte
196196

197197
When enabled, the emitter will not copy example files to the output directory. Instead, it will reference the source example files using relative file paths.
198198

199+
### `type-name-strategy`
200+
201+
**Type:** `"namespaced" | "name-only"`
202+
203+
**Default:** `"namespaced"`
204+
205+
Strategy for naming the OpenAPI names derived from TypeSpec types. "namespaced" (default) includes the namespace prefix for types outside the service namespace (e.g. `LiftrBase.Foo`). "name-only" uses only the type name without any namespace prefix (e.g. `Foo`), reporting an error when two types collapse to the same name.
206+
199207
## Decorators
200208

201209
### Autorest

packages/typespec-autorest/src/emit.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ export function resolveAutorestOptions(
116116
xmlStrategy: resolvedOptions["xml-strategy"],
117117
outputSplitting: resolvedOptions["output-splitting"],
118118
skipExampleCopying: resolvedOptions["skip-example-copying"],
119+
typeNameStrategy: resolvedOptions["type-name-strategy"],
119120
};
120121
}
121122

packages/typespec-autorest/src/lib.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,21 @@ export interface AutorestEmitterOptions {
125125
* @default false
126126
*/
127127
"skip-example-copying"?: boolean;
128+
129+
/**
130+
* Strategy for naming the OpenAPI names derived from TypeSpec types (definition/schema
131+
* names, parameter keys, inline names, `x-typespec-name`, etc.).
132+
*
133+
* - `"namespaced"`: Include the namespace prefix when a type lives outside the service namespace
134+
* (e.g. `LiftrBase.Foo`). The service (and root `TypeSpec`) namespace is always stripped. This
135+
* is the current/default behavior.
136+
* - `"name-only"`: Use only the type name without any namespace prefix (e.g. `Foo`). When two
137+
* types from different namespaces collapse to the same name, the conflict is reported as an
138+
* error (`@typespec/openapi/duplicate-type-name`).
139+
*
140+
* @default "namespaced"
141+
*/
142+
"type-name-strategy"?: "namespaced" | "name-only";
128143
}
129144

130145
const EmitterOptionsSchema: JSONSchemaType<AutorestEmitterOptions> = {
@@ -268,6 +283,14 @@ const EmitterOptionsSchema: JSONSchemaType<AutorestEmitterOptions> = {
268283
description:
269284
"When enabled, the emitter will not copy example files to the output directory. Instead, it will reference the source example files using relative file paths.",
270285
},
286+
"type-name-strategy": {
287+
type: "string",
288+
enum: ["namespaced", "name-only"],
289+
nullable: true,
290+
default: "namespaced",
291+
description:
292+
'Strategy for naming the OpenAPI names derived from TypeSpec types. "namespaced" (default) includes the namespace prefix for types outside the service namespace (e.g. `LiftrBase.Foo`). "name-only" uses only the type name without any namespace prefix (e.g. `Foo`), reporting an error when two types collapse to the same name.',
293+
},
271294
},
272295
required: [],
273296
};

packages/typespec-autorest/src/openapi.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,18 @@ export interface AutorestDocumentEmitterOptions {
262262
* @default false
263263
*/
264264
readonly skipExampleCopying?: boolean;
265+
266+
/**
267+
* Strategy for naming the OpenAPI names derived from TypeSpec types (definition/schema
268+
* names, parameter keys, inline names, `x-typespec-name`, etc.).
269+
*
270+
* - `"namespaced"`: Include the namespace prefix for types outside the service namespace
271+
* (e.g. `LiftrBase.Foo`). Default.
272+
* - `"name-only"`: Use only the type name without any namespace prefix (e.g. `Foo`). Conflicts are
273+
* reported as an error.
274+
* @default "namespaced"
275+
*/
276+
readonly typeNameStrategy?: "namespaced" | "name-only";
265277
}
266278

267279
type HttpParameterProperties = Extract<
@@ -279,6 +291,11 @@ export async function getOpenAPIForService(
279291
const typeNameOptions: TypeNameOptions = {
280292
// shorten type names by removing TypeSpec and service namespace
281293
namespaceFilter(ns) {
294+
// With the "name-only" strategy, strip every namespace so names are not prefixed by their
295+
// namespace (e.g. `Foo` instead of `LiftrBase.Foo`).
296+
if (options.typeNameStrategy === "name-only") {
297+
return false;
298+
}
282299
return !isService(program, ns);
283300
},
284301
};
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { expectDiagnostics } from "@typespec/compiler/testing";
2+
import { describe, expect, it } from "vitest";
3+
import { compileOpenAPI, diagnoseOpenApiFor } from "./test-host.js";
4+
5+
const code = `
6+
@service
7+
namespace MyService;
8+
9+
namespace LiftrBase {
10+
model MarketplaceDetails {}
11+
}
12+
13+
op read(): LiftrBase.MarketplaceDetails;
14+
`;
15+
16+
// Two models with the same name in different namespaces.
17+
const sameNameInDifferentNamespaces = `
18+
@service
19+
namespace MyService;
20+
21+
namespace LiftrBase {
22+
model Widget {}
23+
}
24+
namespace Other {
25+
model Widget {}
26+
}
27+
28+
@route("/a") op a(): LiftrBase.Widget;
29+
@route("/b") op b(): Other.Widget;
30+
`;
31+
32+
describe("default (namespaced)", () => {
33+
it("keeps the namespace prefix in the definition name", async () => {
34+
const oapi = await compileOpenAPI(code);
35+
expect(oapi.definitions).toHaveProperty(["LiftrBase.MarketplaceDetails"]);
36+
});
37+
38+
it("is the behavior when type-name-strategy is explicitly namespaced", async () => {
39+
const oapi = await compileOpenAPI(code, { options: { "type-name-strategy": "namespaced" } });
40+
expect(oapi.definitions).toHaveProperty(["LiftrBase.MarketplaceDetails"]);
41+
});
42+
43+
it("same name in different namespaces do not conflict", async () => {
44+
const oapi = await compileOpenAPI(sameNameInDifferentNamespaces);
45+
expect(oapi.definitions).toHaveProperty(["LiftrBase.Widget"]);
46+
expect(oapi.definitions).toHaveProperty(["Other.Widget"]);
47+
});
48+
});
49+
50+
describe("name-only", () => {
51+
it("removes the namespace from the definition name", async () => {
52+
const oapi = await compileOpenAPI(code, { options: { "type-name-strategy": "name-only" } });
53+
expect(oapi.definitions).toHaveProperty("MarketplaceDetails");
54+
expect(oapi.definitions).not.toHaveProperty(["LiftrBase.MarketplaceDetails"]);
55+
});
56+
57+
it("reports an error when two types collapse to the same name", async () => {
58+
const diagnostics = await diagnoseOpenApiFor(sameNameInDifferentNamespaces, {
59+
"type-name-strategy": "name-only",
60+
});
61+
62+
expectDiagnostics(diagnostics, [{ code: "@typespec/openapi/duplicate-type-name" }]);
63+
});
64+
65+
it("still honors @friendlyName over the name-only strategy", async () => {
66+
const oapi = await compileOpenAPI(
67+
`
68+
@service
69+
namespace MyService;
70+
71+
namespace LiftrBase {
72+
@friendlyName("CustomName")
73+
model MarketplaceDetails {}
74+
}
75+
76+
op read(): LiftrBase.MarketplaceDetails;
77+
`,
78+
{ options: { "type-name-strategy": "name-only" } },
79+
);
80+
expect(oapi.definitions).toHaveProperty("CustomName");
81+
});
82+
});

website/src/content/docs/docs/emitters/typespec-autorest/reference/emitter.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,3 +193,11 @@ Determines whether output should be split into multiple files. The only supporte
193193
**Default:** `false`
194194

195195
When enabled, the emitter will not copy example files to the output directory. Instead, it will reference the source example files using relative file paths.
196+
197+
### `type-name-strategy`
198+
199+
**Type:** `"namespaced" | "name-only"`
200+
201+
**Default:** `"namespaced"`
202+
203+
Strategy for naming the OpenAPI names derived from TypeSpec types. "namespaced" (default) includes the namespace prefix for types outside the service namespace (e.g. `LiftrBase.Foo`). "name-only" uses only the type name without any namespace prefix (e.g. `Foo`), reporting an error when two types collapse to the same name.

0 commit comments

Comments
 (0)