Skip to content
Draft
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: fix
packages:
- "@typespec/openapi3"
---

Preserve distinct discriminated union envelopes when variants share the same payload type.
10 changes: 5 additions & 5 deletions packages/openapi3/src/schema-emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -571,11 +571,11 @@ export class OpenAPI3SchemaEmitterBase<
}

/**
* Mapping of cached envelope models for union variants.
* Envelope models are keyed by variant name, since variants can share a payload type.
*/
#unionVariantEnvelopeVisibilityMap: WeakMap<
Union,
WeakMap<Type, { default: Model; byVisibility: Map<Visibility, Model> }>
Map<string, { default: Model; byVisibility: Map<Visibility, Model> }>
> = new WeakMap();

/**
Expand All @@ -601,15 +601,15 @@ export class OpenAPI3SchemaEmitterBase<
let map = this.#unionVariantEnvelopeVisibilityMap.get(union.type);

if (!map) {
map = new WeakMap();
map = new Map();
this.#unionVariantEnvelopeVisibilityMap.set(union.type, map);
}

let entry = map.get(variant);
let entry = map.get(variantName);
if (!entry) {
// Initialize entry
entry = { default: createEnvelopeModel(), byVisibility: new Map() };
map.set(variant, entry);
map.set(variantName, entry);

// Manually track the model's usage according to the union's usage.
if (usage) this._visibilityUsage.manuallyTrack(entry.default, usage);
Expand Down
129 changes: 129 additions & 0 deletions packages/openapi3/test/union-schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,135 @@ worksFor(supportedVersions, ({ diagnoseOpenApiFor, oapiForModel, openApiFor }) =
});
});

it.each([
{ type: "string", declaration: "", schema: { type: "string" } },
{
type: "Payload",
declaration: "model Payload { value: string; }",
schema: { $ref: "#/components/schemas/Payload" },
},
])(
"preserves distinct envelopes for variants sharing $type",
async ({ type, declaration, schema }) => {
const res = await openApiFor(
`
@service namespace Example;
${declaration}

@discriminated(#{
discriminatorPropertyName: "tag",
envelopePropertyName: "contents"
})
union Limit {
daily: ${type},
weekly: ${type},
monthly: ${type},
}

@route("/limit") @get op read(): Limit;
@route("/limit") @post op write(@body body: Limit): void;
`,
);

deepStrictEqual(res.components.schemas.Limit, {
type: "object",
oneOf: [
{ $ref: "#/components/schemas/LimitDaily" },
{ $ref: "#/components/schemas/LimitWeekly" },
{ $ref: "#/components/schemas/LimitMonthly" },
],
discriminator: {
propertyName: "tag",
mapping: {
daily: "#/components/schemas/LimitDaily",
weekly: "#/components/schemas/LimitWeekly",
monthly: "#/components/schemas/LimitMonthly",
},
},
});
for (const [name, tag] of [
["LimitDaily", "daily"],
["LimitWeekly", "weekly"],
["LimitMonthly", "monthly"],
]) {
deepStrictEqual(res.components.schemas[name], {
type: "object",
properties: {
tag: { type: "string", enum: [tag] },
contents: schema,
},
required: ["tag", "contents"],
});
}
},
);

it("preserves distinct envelopes for variants sharing a visibility-transformed model", async () => {
const res = await openApiFor(
`
model A {
value: string;
@visibility(Lifecycle.Create)
co: string;
}

@discriminated
union U {
a: A,
b: A,
}

@put op update(@body data: U): U;
`,
);

for (const suffix of ["", "CreateOrUpdate"]) {
deepStrictEqual(res.components.schemas[`U${suffix}`], {
type: "object",
oneOf: [
{ $ref: `#/components/schemas/UA${suffix}` },
{ $ref: `#/components/schemas/UB${suffix}` },
],
discriminator: {
propertyName: "kind",
mapping: {
a: `#/components/schemas/UA${suffix}`,
b: `#/components/schemas/UB${suffix}`,
},
},
});
for (const [name, kind] of [
["UA", "a"],
["UB", "b"],
]) {
deepStrictEqual(res.components.schemas[`${name}${suffix}`], {
type: "object",
properties: {
kind: { type: "string", enum: [kind] },
value: { $ref: `#/components/schemas/A${suffix}` },
},
required: ["kind", "value"],
});
}
}
deepStrictEqual(res.components.schemas.A, {
type: "object",
properties: { value: { type: "string" } },
required: ["value"],
});
deepStrictEqual(res.components.schemas.ACreateOrUpdate, {
type: "object",
properties: { value: { type: "string" }, co: { type: "string" } },
required: ["value", "co"],
});
deepStrictEqual(res.paths["/"].put.requestBody.content["application/json"].schema, {
$ref: "#/components/schemas/UCreateOrUpdate",
});
deepStrictEqual(res.paths["/"].put.responses["200"].content["application/json"].schema, {
$ref: "#/components/schemas/U",
});
});

it("envelope none", async () => {
const res = await openApiFor(
`
Expand Down
Loading