diff --git a/.chronus/changes/openapi3-fix-circ-ref-unions-2024-6-19-10-0-54.md b/.chronus/changes/openapi3-fix-circ-ref-unions-2024-6-19-10-0-54.md new file mode 100644 index 00000000000..2d5d19ad2ad --- /dev/null +++ b/.chronus/changes/openapi3-fix-circ-ref-unions-2024-6-19-10-0-54.md @@ -0,0 +1,7 @@ +--- +changeKind: fix +packages: + - "@typespec/openapi3" +--- + +Fixes bug where circular references in unions caused an empty object to be emitted instead of a ref. \ No newline at end of file diff --git a/packages/openapi3/src/schema-emitter.ts b/packages/openapi3/src/schema-emitter.ts index 212f9a4ea70..3340fa57d33 100644 --- a/packages/openapi3/src/schema-emitter.ts +++ b/packages/openapi3/src/schema-emitter.ts @@ -521,25 +521,16 @@ export class OpenAPI3SchemaEmitter extends TypeEmitter< } } - if (schemaMembers.length === 0) { - if (nullable) { - // This union is equivalent to just `null` but OA3 has no way to specify - // null as a value, so we throw an error. - reportDiagnostic(program, { code: "union-null", target: union }); - return new ObjectBuilder({}); - } else { - // completely empty union can maybe only happen with bugs? - compilerAssert(false, "Attempting to emit an empty union"); - } - } - - if (schemaMembers.length === 1) { + const wrapWithObjectBuilder = ( + schemaMember: { schema: any; type: Type | null }, + { applyNullable }: { applyNullable: boolean } + ): ObjectBuilder => { // we can just return the single schema member after applying nullable - const schema = schemaMembers[0].schema; - const type = schemaMembers[0].type; + const schema = schemaMember.schema; + const type = schemaMember.type; const additionalProps: Partial = this.#applyConstraints(union, {}); - if (nullable) { + if (applyNullable && nullable) { additionalProps.nullable = true; } @@ -567,10 +558,26 @@ export class OpenAPI3SchemaEmitter extends TypeEmitter< return merged; } } + }; + + if (schemaMembers.length === 0) { + if (nullable) { + // This union is equivalent to just `null` but OA3 has no way to specify + // null as a value, so we throw an error. + reportDiagnostic(program, { code: "union-null", target: union }); + return new ObjectBuilder({}); + } else { + // completely empty union can maybe only happen with bugs? + compilerAssert(false, "Attempting to emit an empty union"); + } + } + + if (schemaMembers.length === 1) { + return wrapWithObjectBuilder(schemaMembers[0], { applyNullable: true }); } const schema: OpenAPI3Schema = { - [ofType]: schemaMembers.map((m) => m.schema), + [ofType]: schemaMembers.map((m) => wrapWithObjectBuilder(m, { applyNullable: false })), }; if (nullable) { diff --git a/packages/openapi3/test/circular-references.test.ts b/packages/openapi3/test/circular-references.test.ts index ada605bbb40..b677c719723 100644 --- a/packages/openapi3/test/circular-references.test.ts +++ b/packages/openapi3/test/circular-references.test.ts @@ -34,4 +34,20 @@ describe("openapi3: circular reference", () => { }, }); }); + + it("can reference itself via a union property", async () => { + const res = await oapiForModel( + "Pet", + ` + model Pet { parents?: string | Pet }; + ` + ); + + deepStrictEqual(res.schemas.Pet, { + type: "object", + properties: { + parents: { anyOf: [{ type: "string" }, { $ref: "#/components/schemas/Pet" }] }, + }, + }); + }); });