Commit 622804c
fix(openapi3): missing discriminator mapping entry when first union variant causes circular emit (#10268)
## Problem
When a TypeSpec model uses `@discriminator` on a base type and all
subtypes are referenced through a named union, the first variant in the
union is silently dropped from the generated `discriminator.mapping` in
the OpenAPI output.
**Example:**
```typespec
@Discriminator("classification")
model Pokemon { classification: string; }
model NormalPokemon extends Pokemon { classification: "normal"; }
model LegendaryPokemon extends Pokemon { classification: "legendary"; }
model MythicalPokemon extends Pokemon { classification: "mythical"; }
union PokemonVariant {
normal: NormalPokemon,
legendary: LegendaryPokemon,
mythical: MythicalPokemon,
}
```
**Generated (broken):**
```yaml
discriminator:
propertyName: classification
mapping:
legendary: '#/components/schemas/LegendaryPokemon'
mythical: '#/components/schemas/MythicalPokemon'
# 'normal' is missing!
```
## Root Cause
`getDiscriminatorMapping` calls `emitTypeReference` for each derived
model. The first variant in the union (`NormalPokemon`) is already
mid-emission when `applyDiscriminator` runs — because emitting
`NormalPokemon` triggered the emission of its base `Pokemon`, which
immediately calls `applyDiscriminator`.
For a model that is currently being emitted, `emitTypeReference` returns
a `Placeholder` (to handle the circular reference). The existing code
does:
```ts
mapping[key] = (ref.value as any).$ref;
```
`Placeholder` has no `$ref` property, so this evaluates to `undefined`.
The `Placeholder` does resolve correctly later (via its `onValue`
callback mechanism), but nobody registered a listener to update the
mapping — so `mapping["normal"]` stays `undefined` and is silently
dropped during YAML serialisation.
The second and third variants (`LegendaryPokemon`, `MythicalPokemon`)
are not yet in the emit stack at this point, so their
`emitTypeReference` calls return proper declarations and their `$ref`
values are captured correctly.
## Fix
Check whether `ref.value` is a `Placeholder`. If so, register an
`onValue` listener that writes the resolved `$ref` into the mapping once
the circular reference unwinds. `Placeholder` is already imported in
this file.
```ts
getDiscriminatorMapping(variants: Map<string, Type>) {
const mapping: Record<string, string> | undefined = {};
for (const [key, model] of variants.entries()) {
const ref = this.emitter.emitTypeReference(model);
compilerAssert(ref.kind === "code", "Unexpected ref schema. Should be kind: code");
if (ref.value instanceof Placeholder) {
ref.value.onValue((resolvedValue) => {
mapping[key] = (resolvedValue as any).$ref;
});
} else {
mapping[key] = (ref.value as any).$ref;
}
}
return mapping;
}
```
The `onValue` callback fires before YAML serialisation, so the mapping
object is complete by the time the output is written.
---------
Co-authored-by: Timothee Guerin <tiguerin@microsoft.com>1 parent 9e6d6bf commit 622804c
4 files changed
Lines changed: 49 additions & 2 deletions
File tree
- .chronus/changes
- packages/openapi3
- src
- test
Lines changed: 7 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
3 | 3 | | |
4 | 4 | | |
5 | 5 | | |
| 6 | + | |
6 | 7 | | |
7 | 8 | | |
8 | 9 | | |
| |||
98 | 99 | | |
99 | 100 | | |
100 | 101 | | |
101 | | - | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
102 | 109 | | |
103 | 110 | | |
104 | 111 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
720 | 720 | | |
721 | 721 | | |
722 | 722 | | |
723 | | - | |
| 723 | + | |
| 724 | + | |
| 725 | + | |
| 726 | + | |
| 727 | + | |
| 728 | + | |
| 729 | + | |
724 | 730 | | |
725 | 731 | | |
726 | 732 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
345 | 345 | | |
346 | 346 | | |
347 | 347 | | |
| 348 | + | |
| 349 | + | |
| 350 | + | |
| 351 | + | |
| 352 | + | |
| 353 | + | |
| 354 | + | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
| 362 | + | |
| 363 | + | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
| 369 | + | |
| 370 | + | |
| 371 | + | |
| 372 | + | |
| 373 | + | |
| 374 | + | |
348 | 375 | | |
349 | 376 | | |
350 | 377 | | |
| |||
0 commit comments