diff --git a/packages/openapi-generator/src/openapi.ts b/packages/openapi-generator/src/openapi.ts index c9efd035..91c35379 100644 --- a/packages/openapi-generator/src/openapi.ts +++ b/packages/openapi-generator/src/openapi.ts @@ -44,7 +44,14 @@ function schemaToOpenAPI( // Or should we just conflate explicit null and undefined properties? return { nullable: true, enum: [] }; case 'ref': - return { $ref: `#/components/schemas/${schema.name}` }; + // if defaultOpenAPIObject is empty, no need to wrap the $ref in an allOf array + if (Object.keys(defaultOpenAPIObject).length === 0) { + return { $ref: `#/components/schemas/${schema.name}` }; + } + return { + allOf: [{ $ref: `#/components/schemas/${schema.name}` }], + ...defaultOpenAPIObject, + }; case 'array': const innerSchema = schemaToOpenAPI(schema.items); if (innerSchema === undefined) { diff --git a/packages/openapi-generator/src/optimize.ts b/packages/openapi-generator/src/optimize.ts index f31b1cfe..8cb2b929 100644 --- a/packages/openapi-generator/src/optimize.ts +++ b/packages/openapi-generator/src/optimize.ts @@ -153,6 +153,11 @@ export function optimize(schema: Schema): Schema { } else if (schema.type === 'tuple') { const schemas = schema.schemas.map(optimize); return { type: 'tuple', schemas }; + } else if (schema.type === 'ref') { + if (schema.comment) { + return { ...schema, comment: schema.comment }; + } + return schema; } else { return schema; } diff --git a/packages/openapi-generator/test/openapi.test.ts b/packages/openapi-generator/test/openapi.test.ts index 61ac929c..5a9b56cc 100644 --- a/packages/openapi-generator/test/openapi.test.ts +++ b/packages/openapi-generator/test/openapi.test.ts @@ -2180,3 +2180,149 @@ testCase('route with descriptions, patterns, and examples', ROUTE_WITH_DESCRIPTI schemas: {} } }); + +const ROUTE_WITH_DESCRIPTIONS_FOR_REFERENCES = ` +import * as t from 'io-ts'; +import * as h from '@api-ts/io-ts-http'; + +const Foo = t.type({ foo: t.string }); +const Bar = t.type({ bar: t.number }); + +/** + * A simple route with type descriptions for references + * + * @operationId api.v1.test + * @tag Test Routes + */ +export const route = h.httpRoute({ + path: '/foo', + method: 'GET', + request: h.httpRequest({ + query: { + bar: t.array(t.string), + }, + body: { + /** + * This is a foo description. + * @example "BitGo Inc" + */ + foo: Foo, + bar: Bar, + }, + }), + response: { + 200: { + test: t.string + } + }, +}); +`; + +testCase('route with descriptions for references', ROUTE_WITH_DESCRIPTIONS_FOR_REFERENCES, { + openapi: '3.0.3', + info: { + title: 'Test', + version: '1.0.0' + }, + paths: { + '/foo': { + get: { + summary: 'A simple route with type descriptions for references', + operationId: 'api.v1.test', + tags: [ + 'Test Routes' + ], + parameters: [ + { + name: 'bar', + in: 'query', + required: true, + schema: { + type: 'array', + items: { + type: 'string' + } + } + } + ], + requestBody: { + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + // needs to be wrapped in an allOf to preserve the description + foo: { + allOf: [ + { + $ref: '#/components/schemas/Foo' + } + ], + description: 'This is a foo description.', + example: 'BitGo Inc' + }, + // should not need to be wrapped in an allOf + bar: { + $ref: '#/components/schemas/Bar' + } + }, + required: [ + 'foo', + 'bar' + ] + } + } + } + }, + responses: { + '200': { + description: 'OK', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + test: { + type: 'string' + } + }, + required: [ + 'test' + ] + } + } + } + } + } + } + } + }, + components: { + schemas: { + Foo: { + title: 'Foo', + type: 'object', + properties: { + foo: { + type: 'string' + } + }, + required: [ + 'foo' + ] + }, + Bar: { + title: 'Bar', + type: 'object', + properties: { + bar: { + type: 'number' + } + }, + required: [ + 'bar' + ] + } + } + } +});