Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: feature
packages:
- "@azure-tools/typespec-client-generator-core"
---

Convert inline-discriminated unions with explicit nominal model hierarchies to polymorphic SDK models by default. Set `convertDiscriminatedUnionsToModels: false` in `createSdkContext` to preserve union representations. Ordinary unions and object-envelope unions retain their existing behavior.
4 changes: 4 additions & 0 deletions packages/typespec-client-generator-core/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ export function createTCGCContext(

previewStringRegex: /-preview$/,
disableUsageAccessPropagationToBase: false,
convertDiscriminatedUnionsToModels: true,
generateProtocolMethods: true,
generateConvenienceMethods: true,
__referencedTypeCache: new Map<
Expand Down Expand Up @@ -173,6 +174,8 @@ export interface CreateSdkContextOptions {
exportTCGCoutput?: boolean; // this flag is for emitter to export TCGC output as yaml file
flattenUnionAsEnum?: boolean; // this flag is for emitter to decide whether tcgc should flatten union as enum
enableLegacyHierarchyBuilding?: boolean; // this flag is for emitter to decide whether tcgc should respect the `@hierarchyBuilding` decorator
/** Convert eligible inline-discriminated unions to model hierarchies. Default: true. */
convertDiscriminatedUnionsToModels?: boolean;
}

export async function createSdkContext<
Expand Down Expand Up @@ -238,6 +241,7 @@ export async function createSdkContext<
disableUsageAccessPropagationToBase: options?.disableUsageAccessPropagationToBase ?? false,
flattenUnionAsEnum: options?.flattenUnionAsEnum ?? true,
enableLegacyHierarchyBuilding: options?.enableLegacyHierarchyBuilding ?? true,
convertDiscriminatedUnionsToModels: options?.convertDiscriminatedUnionsToModels ?? true,
};

if (context.options["examples-dir"]) {
Expand Down
Comment thread
tadelesh marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
This diagnostic is issued when an inline-discriminated union's discriminator property or tag mappings conflict with another union or an existing model discriminator in the same hierarchy.

## Impact

- **Area:** SDK union type conversion. Generation continues with the union representation instead of overwriting conflicting discriminator metadata.
- **Not affected:** The TypeSpec union declaration, service routes, and wire payloads are unchanged.

## ❌ Incorrect Usage

```typespec
@service
namespace Example;

model PetBase {
name: string;
}

model Cat extends PetBase {
meows: boolean;
}

@discriminated(#{ envelope: "none" })
union Pet extends PetBase {
cat: Cat,
feline: Cat, // one model cannot have two polymorphic discriminator values
}

op get(): Pet;
```

## Diagnostic Message

TCGC reports:

```text
Cannot convert discriminated union 'Pet' to a model hierarchy because its discriminator metadata conflicts with another union or model using the same hierarchy. The union is preserved. Set convertDiscriminatedUnionsToModels to false to retain union representations.
```

## ✅ How to Fix

Use consistent discriminator properties and tag mappings throughout the shared hierarchy. Give each alternative with a different wire tag its own model.

```typespec
@service
namespace Example;

model PetBase {
name: string;
}

model Cat extends PetBase {
meows: boolean;
}

model Dog extends PetBase {
barks: boolean;
}

@discriminated(#{ envelope: "none" })
union Pet extends PetBase {
cat: Cat,
dog: Dog,
}

op get(): Pet;
```

Emitters that intentionally use native discriminated unions can set `convertDiscriminatedUnionsToModels: false` in `createSdkContext` instead of requesting polymorphic conversion.

## Suppression

This diagnostic should not be suppressed. Align the hierarchy's discriminator metadata, or disable conversion when the emitter intentionally retains discriminated unions.
5 changes: 5 additions & 0 deletions packages/typespec-client-generator-core/src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,12 @@ export interface TCGCContext {
disableUsageAccessPropagationToBase: boolean;
flattenUnionAsEnum?: boolean;
enableLegacyHierarchyBuilding?: boolean;
convertDiscriminatedUnionsToModels?: boolean;

__discriminatedUnionModels?: Map<Union, Model | undefined>;
__discriminatedUnionConversionDepth?: number;
__deferDiscriminatedUnionConversion?: boolean;
__pendingDiscriminatedUnionConversion?: boolean;
__referencedTypeCache: Map<Type, SdkModelType | SdkEnumType | SdkUnionType | SdkNullableType>;
__arrayDictionaryCache: Map<Type, SdkDictionaryType | SdkArrayType>;
__methodParameterCache: Map<ModelProperty, SdkMethodParameter>;
Expand Down
7 changes: 7 additions & 0 deletions packages/typespec-client-generator-core/src/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,13 @@ function doc(code: string) {
export const $lib = createTypeSpecLibrary({
name: "@azure-tools/typespec-client-generator-core",
diagnostics: {
"union-model-conversion-conflict": {
...doc("union-model-conversion-conflict"),
severity: "warning",
messages: {
default: paramMessage`Cannot convert discriminated union '${"unionName"}' to a model hierarchy because its discriminator metadata conflicts with another union or model using the same hierarchy. The union is preserved. Set convertDiscriminatedUnionsToModels to false to retain union representations.`,
},
},
"union-null": {
...doc("union-null"),
severity: "warning",
Expand Down
Loading
Loading