diff --git a/packages/openapi-generator/src/optimize.ts b/packages/openapi-generator/src/optimize.ts index dc652c4c..181c8362 100644 --- a/packages/openapi-generator/src/optimize.ts +++ b/packages/openapi-generator/src/optimize.ts @@ -1,5 +1,5 @@ import { combineComments } from './comments'; -import { isPrimitive, type Primitive, type Schema } from './ir'; +import { isPrimitive, type CombinedType, type Primitive, type Schema } from './ir'; export type OptimizeFn = (schema: Schema) => Schema; @@ -29,6 +29,28 @@ export function foldIntersection(schema: Schema, optimize: OptimizeFn): Schema { } }); + // If the combined object is empty (no properties) and result is an intersection, + // we can simplify by removing the empty object + const hasProperties = Object.keys(combinedObject.properties).length > 0; + + if (!hasProperties && result !== combinedObject) { + // At this point, result must be an intersection since it was reassigned + const intersectionResult = result as unknown as CombinedType; + + // Remove the empty object from the intersection + const nonEmptySchemas = intersectionResult.schemas.filter( + (s: Schema) => !(s.type === 'object' && Object.keys(s.properties).length === 0), + ); + + if (nonEmptySchemas.length === 0) { + return combinedObject; + } else if (nonEmptySchemas.length === 1) { + return nonEmptySchemas[0]!; + } else { + return { type: 'intersection', schemas: nonEmptySchemas }; + } + } + return result; } diff --git a/packages/openapi-generator/test/optimize.test.ts b/packages/openapi-generator/test/optimize.test.ts index d37b44ba..8bc84dad 100644 --- a/packages/openapi-generator/test/optimize.test.ts +++ b/packages/openapi-generator/test/optimize.test.ts @@ -180,3 +180,72 @@ test('non-consolidatable unions are not consolidated', () => { assert.deepEqual(optimize(input), expected); }); + +test('intersection with non-object types removes empty object', () => { + const input: Schema = { + type: 'intersection', + schemas: [ + { + type: 'object', + properties: {}, + required: [], + }, + { + type: 'ref', + name: 'SomeType', + location: '/path/to/file.ts', + }, + ], + }; + + // The empty object should be removed, leaving just the ref + const expected: Schema = { + type: 'ref', + name: 'SomeType', + location: '/path/to/file.ts', + }; + + assert.deepEqual(optimize(input), expected); +}); + +test('intersection with multiple non-object types removes empty object', () => { + const input: Schema = { + type: 'intersection', + schemas: [ + { + type: 'object', + properties: {}, + required: [], + }, + { + type: 'ref', + name: 'TypeA', + location: '/path/to/file.ts', + }, + { + type: 'ref', + name: 'TypeB', + location: '/path/to/file.ts', + }, + ], + }; + + // The empty object should be removed, leaving the intersection of refs + const expected: Schema = { + type: 'intersection', + schemas: [ + { + type: 'ref', + name: 'TypeA', + location: '/path/to/file.ts', + }, + { + type: 'ref', + name: 'TypeB', + location: '/path/to/file.ts', + }, + ], + }; + + assert.deepEqual(optimize(input), expected); +});