diff --git a/.chronus/changes/fix-duplicate-type-name-visibility-2026-7-28.md b/.chronus/changes/fix-duplicate-type-name-visibility-2026-7-28.md new file mode 100644 index 00000000000..72a2115539c --- /dev/null +++ b/.chronus/changes/fix-duplicate-type-name-visibility-2026-7-28.md @@ -0,0 +1,7 @@ +--- +changeKind: fix +packages: + - "@typespec/openapi3" +--- + +Fix duplicate type name error when a model with a `@visibility(Lifecycle.Create, Lifecycle.Update)` property extends another model. diff --git a/packages/openapi3/src/schema-emitter.ts b/packages/openapi3/src/schema-emitter.ts index 9384fc86a7e..3954702e95b 100644 --- a/packages/openapi3/src/schema-emitter.ts +++ b/packages/openapi3/src/schema-emitter.ts @@ -222,7 +222,13 @@ export class OpenAPI3SchemaEmitterBase< const derivedModels = model.derivedModels.filter(includeDerivedModel); // getSchemaOrRef on all children to push them into components.schemas for (const child of derivedModels) { - this.emitter.emitTypeReference(child); + if (this._visibilityUsage.isUnreachable(child)) { + // Unreachable derived models will be emitted by processUnreferencedSchemas with + // Visibility.All context. Force the same context here to avoid a duplicate declaration. + this.emitter.emitTypeReference(child, { referenceContext: { visibility: Visibility.All } }); + } else { + this.emitter.emitTypeReference(child); + } } this.applyDiscriminator(model, schema as any); diff --git a/packages/openapi3/test/metadata.test.ts b/packages/openapi3/test/metadata.test.ts index 7f5e60a2cd4..18e6cf1ac7b 100644 --- a/packages/openapi3/test/metadata.test.ts +++ b/packages/openapi3/test/metadata.test.ts @@ -1215,6 +1215,33 @@ worksFor(supportedVersions, ({ openApiFor }) => { required: ["name"], }); }); + + it("inheritance tree unreachable with @visibility(Create, Update) property doesn't get conflicts", async () => { + const res = await openApiFor(` + model Base { + } + + model Child extends Base { + @visibility(Lifecycle.Create, Lifecycle.Update) name: string; + } + `); + + deepStrictEqual(Object.keys(res.components.schemas), ["Base", "Child"]); + deepStrictEqual(res.components.schemas.Child, { + type: "object", + allOf: [ + { + $ref: "#/components/schemas/Base", + }, + ], + properties: { + name: { + type: "string", + }, + }, + required: ["name"], + }); + }); }); worksFor(["3.0.0"], ({ openApiFor }) => {