diff --git a/packages/openapi-generator/src/openapi.ts b/packages/openapi-generator/src/openapi.ts index 2350221d..d910d69f 100644 --- a/packages/openapi-generator/src/openapi.ts +++ b/packages/openapi-generator/src/openapi.ts @@ -14,13 +14,27 @@ function schemaToOpenAPI( const createOpenAPIObject = ( schema: Schema, ): OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject | undefined => { + const description = schema.comment?.description; + + const defaultObject = { + ...(description ? { description } : {}), + }; + switch (schema.type) { case 'boolean': case 'string': case 'number': - return { type: schema.type, ...(schema.enum ? { enum: schema.enum } : {}) }; + return { + type: schema.type, + ...(schema.enum ? { enum: schema.enum } : {}), + ...defaultObject, + }; case 'integer': - return { type: 'number', ...(schema.enum ? { enum: schema.enum } : {}) }; + return { + type: 'number', + ...(schema.enum ? { enum: schema.enum } : {}), + ...defaultObject, + }; case 'null': // TODO: OpenAPI v3 does not have an explicit null type, is there a better way to represent this? // Or should we just conflate explicit null and undefined properties? @@ -32,7 +46,7 @@ function schemaToOpenAPI( if (innerSchema === undefined) { return undefined; } - return { type: 'array', items: innerSchema }; + return { type: 'array', items: innerSchema, ...defaultObject }; case 'object': return { type: 'object', @@ -146,13 +160,25 @@ function routeToOpenAPI(route: Route): [string, string, OpenAPIV3.OperationObjec {}, ); + const stripTopLevelComment = (schema: Schema) => { + const copy = { ...schema }; + + if (copy.comment?.description !== undefined && copy.comment?.description !== '') { + copy.comment.description = ''; + } + + return copy; + }; + + const topLevelStripped = stripTopLevelComment(route.body!); + const requestBody = route.body === undefined ? {} : { requestBody: { content: { - 'application/json': { schema: schemaToOpenAPI(route.body) }, + 'application/json': { schema: schemaToOpenAPI(topLevelStripped) }, }, }, }; @@ -174,6 +200,10 @@ function routeToOpenAPI(route: Route): [string, string, OpenAPIV3.OperationObjec // Array types not allowed here const schema = schemaToOpenAPI(p.schema); + if (schema && 'description' in schema) { + delete schema.description; + } + return { name: p.name, ...(p.schema?.comment?.description !== undefined @@ -195,7 +225,7 @@ function routeToOpenAPI(route: Route): [string, string, OpenAPIV3.OperationObjec description, content: { 'application/json': { - schema: schemaToOpenAPI(response), + schema: schemaToOpenAPI(stripTopLevelComment(response)), ...(example !== undefined ? { example } : undefined), }, }, diff --git a/packages/openapi-generator/test/openapi.test.ts b/packages/openapi-generator/test/openapi.test.ts index e5f9e269..b0792348 100644 --- a/packages/openapi-generator/test/openapi.test.ts +++ b/packages/openapi-generator/test/openapi.test.ts @@ -1331,3 +1331,126 @@ testCase('route with multiple unknown tags', ROUTE_WITH_MULTIPLE_UNKNOWN_TAGS, { schemas: {}, }, }); + + +const ROUTE_WITH_TYPE_DESCRIPTIONS = ` +import * as t from 'io-ts'; +import * as h from '@api-ts/io-ts-http'; + +/** + * A simple route with type descriptions + * + * @operationId api.v1.test + * @tag Test Routes + */ +export const route = h.httpRoute({ + path: '/foo', + method: 'GET', + request: h.httpRequest({ + query: { + /** bar param */ + bar: t.string, + }, + body: { + /** foo description */ + foo: t.string, + /** bar description */ + bar: t.number, + child: { + /** child description */ + child: t.string, + } + }, + }), + response: { + 200: { + test: t.string + } + }, +}); +`; + +testCase('route with type descriptions', ROUTE_WITH_TYPE_DESCRIPTIONS, { + openapi: '3.0.3', + info: { + title: 'Test', + version: '1.0.0', + }, + paths: { + '/foo': { + get: { + summary: 'A simple route with type descriptions', + operationId: 'api.v1.test', + tags: ['Test Routes'], + parameters: [ + { + description: 'bar param', + in: 'query', + name: 'bar', + required: true, + schema: { + type: 'string' + } + } + ], + requestBody: { + content: { + 'application/json': { + schema: { + properties: { + bar: { + description: 'bar description', + type: 'number' + }, + child: { + properties: { + child: { + description: 'child description', + type: 'string' + } + }, + required: [ + 'child' + ], + type: 'object' + }, + foo: { + description: 'foo description', + type: 'string' + } + }, + required: [ + 'foo', + 'bar', + 'child' + ], + type: 'object' + } + } + } + }, + responses: { + 200: { + description: 'OK', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + test: { + type: 'string', + }, + }, + required: ['test'], + }, + }, + }, + }, + }, + }, + }, + }, + components: { + schemas: {}, + }, +});