From 12e73c66fbc36053f5bca1c70eeb1c3953db8866 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Jul 2026 18:34:24 +0000 Subject: [PATCH 1/2] Initial plan From 473a9326c6a5809039adaba6dcf3646fc1e207a5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Jul 2026 19:06:15 +0000 Subject: [PATCH 2/2] fix(openapi3): Fix duplicate type name for unreachable derived model with visibility properties When a model with @visibility(Lifecycle.Create, Lifecycle.Update) properties extends another model and both are unreachable from HTTP operations, processUnreferencedSchemas would emit the derived model with Visibility.All context, while modelDeclaration would also emit it by inheriting the parent's reduced Read context. These two different contexts resulted in two schema declarations with the same name, causing a 'Duplicate type name' error. Fix: Emit unreachable derived models with explicit Visibility.All context in modelDeclaration to match the context used by processUnreferencedSchemas, ensuring the TypeEmitter cache deduplication works correctly. Fixes #9727 Co-authored-by: timotheeguerin <1031227+timotheeguerin@users.noreply.github.com> --- ...uplicate-type-name-visibility-2026-7-28.md | 7 +++++ packages/openapi3/src/schema-emitter.ts | 8 +++++- packages/openapi3/test/metadata.test.ts | 27 +++++++++++++++++++ 3 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 .chronus/changes/fix-duplicate-type-name-visibility-2026-7-28.md 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 }) => {