diff --git a/.chronus/changes/allow-clientoption-model-value-2026-08-20-12-50-00.md b/.chronus/changes/allow-clientoption-model-value-2026-08-20-12-50-00.md
new file mode 100644
index 0000000000..7789194018
--- /dev/null
+++ b/.chronus/changes/allow-clientoption-model-value-2026-08-20-12-50-00.md
@@ -0,0 +1,7 @@
+---
+changeKind: feature
+packages:
+ - "@azure-tools/typespec-client-generator-core"
+---
+
+`@clientOption`'s `value` can now reference a TypeSpec model, in addition to `string`, `boolean`, and `number` literal values. The referenced model (including customizations such as `@alternateType`) is preserved and resolved so scoped emitters can access it via `getClientOptions`.
diff --git a/packages/typespec-client-generator-core/README.md b/packages/typespec-client-generator-core/README.md
index c1d7a73fa0..875e9ab4cf 100644
--- a/packages/typespec-client-generator-core/README.md
+++ b/packages/typespec-client-generator-core/README.md
@@ -877,7 +877,7 @@ See supported client options for each language emitter here https://azure.github
warning if no scope is provided (since options are typically language-specific).
```typespec
-@Azure.ClientGenerator.Core.clientOption(name: valueof string, value: valueof unknown, scope?: valueof string)
+@Azure.ClientGenerator.Core.clientOption(name: valueof string, value: unknown | valueof unknown, scope?: valueof string)
```
##### Target
@@ -887,11 +887,11 @@ The type you want to apply the option to.
##### Parameters
-| Name | Type | Description |
-| ----- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| name | `valueof string` | The name of the option (e.g., "enableFeatureFoo"). |
-| value | `valueof unknown` | The value of the option. Can be any type; emitters will cast as needed. |
-| scope | `valueof string` | Specifies the target language emitters to which the decorator applies. Every use must provide an explicit scope; omitting it produces an additional warning.
**Supported language identifiers:** `csharp`, `python`, `java`, `javascript`, `go`, and other language emitter names (derived from the emitter package name, e.g., `@azure-tools/typespec-csharp` → `csharp`).
**Valid patterns:**
- Single language: `"python"`
- Multiple languages (comma-separated): `"python, java"`
- Negation to exclude languages: `"!csharp"` or `"!(java, python)"` |
+| Name | Type | Description |
+| ----- | ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| name | `valueof string` | The name of the option (e.g., "enableFeatureFoo"). |
+| value | `unknown` \| `valueof unknown` | The value of the option. Can be a literal value (string, boolean, number, etc.) or a
reference to a TypeSpec model, in which case the referenced model (including its own decorators,
such as `@alternateType`) is preserved so the scoped emitter can resolve it. Emitters will cast as needed. |
+| scope | `valueof string` | Specifies the target language emitters to which the decorator applies. Every use must provide an explicit scope; omitting it produces an additional warning.
**Supported language identifiers:** `csharp`, `python`, `java`, `javascript`, `go`, and other language emitter names (derived from the emitter package name, e.g., `@azure-tools/typespec-csharp` → `csharp`).
**Valid patterns:**
- Single language: `"python"`
- Multiple languages (comma-separated): `"python, java"`
- Negation to exclude languages: `"!csharp"` or `"!(java, python)"` |
##### Examples
@@ -905,6 +905,14 @@ model MyModel {
}
```
+###### Apply an experimental option that references a model
+
+```typespec
+#suppress "@azure-tools/typespec-client-generator-core/client-option" "preview feature for csharp"
+@clientOption("composes", OpenAICreateResponseOptions, "csharp")
+model FoundryCreateResponseOptions {}
+```
+
#### `@convenientAPI`
Whether you want to generate an operation as a convenient method.
diff --git a/packages/typespec-client-generator-core/generated-defs/Azure.ClientGenerator.Core.ts b/packages/typespec-client-generator-core/generated-defs/Azure.ClientGenerator.Core.ts
index 32121f0e50..04f7d00144 100644
--- a/packages/typespec-client-generator-core/generated-defs/Azure.ClientGenerator.Core.ts
+++ b/packages/typespec-client-generator-core/generated-defs/Azure.ClientGenerator.Core.ts
@@ -1089,7 +1089,9 @@ export type ClientDocDecorator = (
*
* @param target The type you want to apply the option to.
* @param name The name of the option (e.g., "enableFeatureFoo").
- * @param value The value of the option. Can be any type; emitters will cast as needed.
+ * @param value The value of the option. Can be a literal value (string, boolean, number, etc.) or a
+ * reference to a TypeSpec model, in which case the referenced model (including its own decorators,
+ * such as `@alternateType`) is preserved so the scoped emitter can resolve it. Emitters will cast as needed.
* @param scope Specifies the target language emitters to which the decorator applies. Every use must provide an explicit scope; omitting it produces an additional warning.
*
* **Supported language identifiers:** `csharp`, `python`, `java`, `javascript`, `go`, and other language emitter names (derived from the emitter package name, e.g., `@azure-tools/typespec-csharp` → `csharp`).
@@ -1106,12 +1108,18 @@ export type ClientDocDecorator = (
* prop: string;
* }
* ```
+ * @example Apply an experimental option that references a model
+ * ```typespec
+ * #suppress "@azure-tools/typespec-client-generator-core/client-option" "preview feature for csharp"
+ * @clientOption("composes", OpenAICreateResponseOptions, "csharp")
+ * model FoundryCreateResponseOptions {}
+ * ```
*/
export type ClientOptionDecorator = (
context: DecoratorContext,
target: Type,
name: string,
- value: unknown,
+ value: Type | unknown,
scope?: string,
) => DecoratorValidatorCallbacks | void;
diff --git a/packages/typespec-client-generator-core/lib/decorators.tsp b/packages/typespec-client-generator-core/lib/decorators.tsp
index 95188ae096..13acf1e668 100644
--- a/packages/typespec-client-generator-core/lib/decorators.tsp
+++ b/packages/typespec-client-generator-core/lib/decorators.tsp
@@ -1178,7 +1178,9 @@ extern dec clientDoc(
*
* @param target The type you want to apply the option to.
* @param name The name of the option (e.g., "enableFeatureFoo").
- * @param value The value of the option. Can be any type; emitters will cast as needed.
+ * @param value The value of the option. Can be a literal value (string, boolean, number, etc.) or a
+ * reference to a TypeSpec model, in which case the referenced model (including its own decorators,
+ * such as `@alternateType`) is preserved so the scoped emitter can resolve it. Emitters will cast as needed.
* @param scope Specifies the target language emitters to which the decorator applies. Every use must provide an explicit scope; omitting it produces an additional warning.
*
* **Supported language identifiers:** `csharp`, `python`, `java`, `javascript`, `go`, and other language emitter names (derived from the emitter package name, e.g., `@azure-tools/typespec-csharp` → `csharp`).
@@ -1196,10 +1198,17 @@ extern dec clientDoc(
* prop: string;
* }
* ```
+ *
+ * @example Apply an experimental option that references a model
+ * ```typespec
+ * #suppress "@azure-tools/typespec-client-generator-core/client-option" "preview feature for csharp"
+ * @clientOption("composes", OpenAICreateResponseOptions, "csharp")
+ * model FoundryCreateResponseOptions {}
+ * ```
*/
extern dec clientOption(
target: unknown,
name: valueof string,
- value: valueof unknown,
+ value: unknown | (valueof unknown),
scope?: valueof string
);
diff --git a/packages/typespec-client-generator-core/src/internal-utils.ts b/packages/typespec-client-generator-core/src/internal-utils.ts
index 064c49201a..15e640fd66 100644
--- a/packages/typespec-client-generator-core/src/internal-utils.ts
+++ b/packages/typespec-client-generator-core/src/internal-utils.ts
@@ -550,7 +550,7 @@ function getDecoratorArgValue(
): [any, readonly Diagnostic[]] {
const diagnostics = createDiagnosticCollector();
if (typeof arg === "object" && arg !== null && "kind" in arg) {
- if (arg.kind === "EnumMember") {
+ if (arg.kind === "EnumMember" || arg.kind === "Model") {
return diagnostics.wrap(diagnostics.pipe(getClientTypeWithDiagnostics(context, arg as any)));
}
if (
diff --git a/packages/typespec-client-generator-core/test/decorators/client-option.test.ts b/packages/typespec-client-generator-core/test/decorators/client-option.test.ts
index c2593fee64..efc0e39c64 100644
--- a/packages/typespec-client-generator-core/test/decorators/client-option.test.ts
+++ b/packages/typespec-client-generator-core/test/decorators/client-option.test.ts
@@ -193,6 +193,63 @@ describe("@clientOption with getClientOptions getter", () => {
strictEqual(typeof numberValue, "number");
});
+ it("should support a model reference as the value", async () => {
+ const { program } = await SimpleTesterWithService.compile(`
+ model ComposedOptions {
+ a: string;
+ }
+
+ #suppress "@azure-tools/typespec-client-generator-core/client-option"
+ @clientOption("composes", ComposedOptions, "csharp")
+ @test
+ model Test {
+ id: string;
+ }
+
+ op getTest(): Test;
+ `);
+
+ const context = await createSdkContextForTester(program, {
+ emitterName: "@azure-tools/typespec-csharp",
+ });
+
+ const sdkModel = context.sdkPackage.models.find((m) => m.name === "Test");
+ ok(sdkModel, "SDK model should exist");
+
+ const value = getClientOptions(sdkModel, "composes") as { kind?: string; name?: string };
+ strictEqual(value?.kind, "model");
+ strictEqual(value?.name, "ComposedOptions");
+ });
+
+ it("should preserve customizations, such as @alternateType, on a model reference value", async () => {
+ const { program } = await SimpleTesterWithService.compile(`
+ @alternateType(string, "csharp")
+ model AlternateOptions {
+ a: string;
+ }
+
+ #suppress "@azure-tools/typespec-client-generator-core/client-option"
+ @clientOption("composes", AlternateOptions, "csharp")
+ @test
+ model Test {
+ id: string;
+ }
+
+ op getTest(): Test;
+ `);
+
+ const context = await createSdkContextForTester(program, {
+ emitterName: "@azure-tools/typespec-csharp",
+ });
+
+ const sdkModel = context.sdkPackage.models.find((m) => m.name === "Test");
+ ok(sdkModel, "SDK model should exist");
+
+ const value = getClientOptions(sdkModel, "composes") as { kind?: string };
+ strictEqual(value?.kind, "string");
+ // @alternateType(string) replaces the referenced model with the builtin `string` type
+ });
+
it("should return client option value for operation", async () => {
const { program } = await SimpleTesterWithService.compile(`
#suppress "@azure-tools/typespec-client-generator-core/client-option"
diff --git a/packages/typespec-go/src/codegen/core/models.ts b/packages/typespec-go/src/codegen/core/models.ts
index 20f26b1e29..f9f092c1c4 100644
--- a/packages/typespec-go/src/codegen/core/models.ts
+++ b/packages/typespec-go/src/codegen/core/models.ts
@@ -136,25 +136,17 @@ export function generateModels(
if (needsJSONPopulateAsString) {
serdeImports.add("reflect");
serdeImports.add("github.com/Azure/azure-sdk-for-go/sdk/azcore");
- serdeTextBody +=
- "func populateAsString(m map[string]any, k string, v any, fn func() string) {\n";
- serdeTextBody += `${indent.get()}${helpers.buildIfBlock(
- indent,
- {
- condition: "v == nil",
- body: (indent) => `${indent.get()}return\n`,
- },
- [
- {
- condition: "azcore.IsNullValue(v)",
- body: (indent) => `${indent.get()}m[k] = nil\n`,
- },
- {
- condition: "!reflect.ValueOf(v).IsNil()",
- body: (indent) => `${indent.get()}m[k] = fn()\n`,
- },
- ],
- )}\n`;
+ serdeTextBody += "func populateAsString(m map[string]any, k string, v any, fn func() string) {\n";
+ serdeTextBody += `${indent.get()}${helpers.buildIfBlock(indent, {
+ condition: "v == nil",
+ body: (indent) => `${indent.get()}return\n`,
+ }, [{
+ condition: "azcore.IsNullValue(v)",
+ body: (indent) => `${indent.get()}m[k] = nil\n`,
+ }, {
+ condition: "!reflect.ValueOf(v).IsNil()",
+ body: (indent) => `${indent.get()}m[k] = fn()\n`,
+ }])}\n`;
serdeTextBody += "}\n\n";
}
if (needsJSONPopulateTime) {
@@ -219,8 +211,7 @@ export function generateModels(
}
if (needsJSONUnpopulateFromString) {
serdeImports.add("fmt");
- serdeTextBody +=
- "func unpopulateFromString(data json.RawMessage, fn string, f func(s string) error) error {\n";
+ serdeTextBody += "func unpopulateFromString(data json.RawMessage, fn string, f func(s string) error) error {\n";
serdeTextBody += `${indent.get()}${helpers.buildIfBlock(indent, {
condition: `data == nil || string(data) == "null"`,
body: (indent) => `${indent.get()}return nil\n`,
@@ -233,16 +224,14 @@ export function generateModels(
})}\n`;
serdeTextBody += `${indent.get()}${helpers.buildIfBlock(indent, {
condition: "err != nil",
- body: (indent) =>
- `${indent.get()}return fmt.Errorf("struct field %s: %s", fn, err.Error())\n`,
+ body: (indent) => `${indent.get()}return fmt.Errorf("struct field %s: %s", fn, err.Error())\n`,
})}\n`;
serdeTextBody += `${indent.get()}return nil\n`;
serdeTextBody += "}\n\n";
}
if (needsJSONUnpopulateStringArray) {
serdeImports.add("strings");
- serdeTextBody +=
- "func unpopulateStringArray[T ~string](data json.RawMessage, fn string, v *[]T, d string) error {\n";
+ serdeTextBody += "func unpopulateStringArray[T ~string](data json.RawMessage, fn string, v *[]T, d string) error {\n";
serdeTextBody += `${indent.get()}${helpers.buildIfBlock(indent, {
condition: `data == nil || string(data) == "null"`,
body: (indent) => `${indent.get()}return nil\n`,
@@ -250,8 +239,7 @@ export function generateModels(
serdeTextBody += `${indent.get()}var encodedValue string\n`;
serdeTextBody += `${indent.get()}${helpers.buildIfBlock(indent, {
condition: "err := json.Unmarshal(data, &encodedValue); err != nil",
- body: (indent) =>
- `${indent.get()}return fmt.Errorf("struct field %s: %s", fn, err.Error())\n`,
+ body: (indent) => `${indent.get()}return fmt.Errorf("struct field %s: %s", fn, err.Error())\n`,
})}\n`;
serdeTextBody += `${indent.get()}${helpers.buildIfBlock(indent, {
condition: `encodedValue == ""`,
@@ -263,13 +251,9 @@ export function generateModels(
})}\n`;
serdeTextBody += `${indent.get()}values := strings.Split(encodedValue, d)\n`;
serdeTextBody += `${indent.get()}result := make([]T, len(values))\n`;
- serdeTextBody += `${indent.get()}${helpers.buildForBlock(
- indent,
- "i := range values",
- (indent) => {
- return `${indent.get()}result[i] = T(values[i])\n`;
- },
- )}`;
+ serdeTextBody += `${indent.get()}${helpers.buildForBlock(indent, "i := range values", (indent) => {
+ return `${indent.get()}result[i] = T(values[i])\n`;
+ })}`;
serdeTextBody += `${indent.get()}*v = result\n`;
serdeTextBody += `${indent.get()}return nil\n`;
serdeTextBody += "}\n\n";
@@ -305,32 +289,23 @@ export function generateModels(
if (needsJSONPopulateStringArray) {
serdeImports.add("github.com/Azure/azure-sdk-for-go/sdk/azcore");
serdeImports.add("strings");
- serdeTextBody +=
- "func populateStringArray[T ~string](m map[string]any, k string, v []T, d string) {";
- serdeTextBody += `${indent.get()}${helpers.buildIfBlock(
- indent,
+ serdeTextBody += "func populateStringArray[T ~string](m map[string]any, k string, v []T, d string) {";
+ serdeTextBody += `${indent.get()}${helpers.buildIfBlock(indent, {
+ condition: "azcore.IsNullValue(v)",
+ body: (indent) => `${indent.get()}m[k] = nil\n`,
+ }, [
{
- condition: "azcore.IsNullValue(v)",
- body: (indent) => `${indent.get()}m[k] = nil\n`,
- },
- [
- {
- condition: "v != nil",
- body: (indent) => {
- let controlBlock = `${indent.get()}encodedValue := make([]string, len(v))\n`;
- controlBlock += `${indent.get()}${helpers.buildForBlock(
- indent,
- "i := range v",
- (indent) => {
- return `${indent.get()}encodedValue[i] = string(v[i])\n`;
- },
- )}`;
- controlBlock += `${indent.get()}m[k] = strings.Join(encodedValue, d)\n`;
- return controlBlock;
- },
+ condition: "v != nil",
+ body: (indent) => {
+ let controlBlock = `${indent.get()}encodedValue := make([]string, len(v))\n`;
+ controlBlock += `${indent.get()}${helpers.buildForBlock(indent, "i := range v", (indent) => {
+ return `${indent.get()}encodedValue[i] = string(v[i])\n`;
+ })}`;
+ controlBlock += `${indent.get()}m[k] = strings.Join(encodedValue, d)\n`;
+ return controlBlock;
},
- ],
- )}\n`;
+ }
+ ])}\n`;
serdeTextBody += "}\n\n";
}
if (needsJSONUnpopulateTime || needsJSONPopulateTime) {
diff --git a/website/src/content/docs/docs/libraries/typespec-client-generator-core/reference/decorators.md b/website/src/content/docs/docs/libraries/typespec-client-generator-core/reference/decorators.md
index e46ec1f134..245d4a60b2 100644
--- a/website/src/content/docs/docs/libraries/typespec-client-generator-core/reference/decorators.md
+++ b/website/src/content/docs/docs/libraries/typespec-client-generator-core/reference/decorators.md
@@ -733,7 +733,7 @@ See supported client options for each language emitter here https://azure.github
warning if no scope is provided (since options are typically language-specific).
```typespec
-@Azure.ClientGenerator.Core.clientOption(name: valueof string, value: valueof unknown, scope?: valueof string)
+@Azure.ClientGenerator.Core.clientOption(name: valueof string, value: unknown | valueof unknown, scope?: valueof string)
```
#### Target
@@ -743,11 +743,11 @@ The type you want to apply the option to.
#### Parameters
-| Name | Type | Description |
-| ----- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| name | `valueof string` | The name of the option (e.g., "enableFeatureFoo"). |
-| value | `valueof unknown` | The value of the option. Can be any type; emitters will cast as needed. |
-| scope | `valueof string` | Specifies the target language emitters to which the decorator applies. Every use must provide an explicit scope; omitting it produces an additional warning.
**Supported language identifiers:** `csharp`, `python`, `java`, `javascript`, `go`, and other language emitter names (derived from the emitter package name, e.g., `@azure-tools/typespec-csharp` → `csharp`).
**Valid patterns:**
- Single language: `"python"`
- Multiple languages (comma-separated): `"python, java"`
- Negation to exclude languages: `"!csharp"` or `"!(java, python)"` |
+| Name | Type | Description |
+| ----- | ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| name | `valueof string` | The name of the option (e.g., "enableFeatureFoo"). |
+| value | `unknown` \| `valueof unknown` | The value of the option. Can be a literal value (string, boolean, number, etc.) or a
reference to a TypeSpec model, in which case the referenced model (including its own decorators,
such as `@alternateType`) is preserved so the scoped emitter can resolve it. Emitters will cast as needed. |
+| scope | `valueof string` | Specifies the target language emitters to which the decorator applies. Every use must provide an explicit scope; omitting it produces an additional warning.
**Supported language identifiers:** `csharp`, `python`, `java`, `javascript`, `go`, and other language emitter names (derived from the emitter package name, e.g., `@azure-tools/typespec-csharp` → `csharp`).
**Valid patterns:**
- Single language: `"python"`
- Multiple languages (comma-separated): `"python, java"`
- Negation to exclude languages: `"!csharp"` or `"!(java, python)"` |
#### Examples
@@ -761,6 +761,14 @@ model MyModel {
}
```
+##### Apply an experimental option that references a model
+
+```typespec
+#suppress "@azure-tools/typespec-client-generator-core/client-option" "preview feature for csharp"
+@clientOption("composes", OpenAICreateResponseOptions, "csharp")
+model FoundryCreateResponseOptions {}
+```
+
### `@convenientAPI` {#@Azure.ClientGenerator.Core.convenientAPI}
Whether you want to generate an operation as a convenient method.