From ba6977efd680f22455fce1be4dc43aa37901edce Mon Sep 17 00:00:00 2001 From: Young Jun Joo Date: Wed, 5 Mar 2025 15:16:14 -0500 Subject: [PATCH 1/2] Fix integer type representation in OpenAPI schema generation This commit fixes an issue where query parameters with integer types were incorrectly being represented as 'number' type in the generated OpenAPI specifications. Now integer types are properly represented as 'integer' in the schema, ensuring that tools consuming the OpenAPI specification correctly understand the expected data type of query parameters. --- packages/openapi-generator/src/openapi.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/openapi-generator/src/openapi.ts b/packages/openapi-generator/src/openapi.ts index a7fe83de..7f0d3736 100644 --- a/packages/openapi-generator/src/openapi.ts +++ b/packages/openapi-generator/src/openapi.ts @@ -20,18 +20,23 @@ export function schemaToOpenAPI( switch (schema.type) { case 'boolean': case 'string': - case 'number': return { type: schema.type, ...(schema.enum ? { enum: schema.enum } : {}), ...defaultOpenAPIObject, }; - case 'integer': + case 'number': return { type: 'number', ...(schema.enum ? { enum: schema.enum } : {}), ...defaultOpenAPIObject, }; + case 'integer': + return { + type: 'integer', + ...(schema.enum ? { enum: schema.enum } : {}), + ...defaultOpenAPIObject, + }; 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? From 484690acf3314bc5c3417f299d9ab6a14c397c6d Mon Sep 17 00:00:00 2001 From: Young Jun Joo Date: Fri, 7 Mar 2025 10:00:51 -0500 Subject: [PATCH 2/2] fix: represent integer query parameters with correct type Update OpenAPI schema generation to properly represent integer query parameters as 'type: integer' instead of 'type: number'. This ensures API consumers receive accurate type information for validation and documentation purposes. --- packages/openapi-generator/src/jsdoc.ts | 2 +- .../openapi-generator/src/knownImports.ts | 1 + .../test/openapi/base.test.ts | 122 ++++++++++++++++++ 3 files changed, 124 insertions(+), 1 deletion(-) diff --git a/packages/openapi-generator/src/jsdoc.ts b/packages/openapi-generator/src/jsdoc.ts index 6bf698f5..532c7cca 100644 --- a/packages/openapi-generator/src/jsdoc.ts +++ b/packages/openapi-generator/src/jsdoc.ts @@ -43,7 +43,7 @@ export function parseCommentBlock(comment: Block): JSDoc { if (line.tokens.description === '') { continue; } - summary = line.tokens.description; + summary = line.tokens.description.trim(); } else { description = `${description ?? ''}\n${line.tokens.description}`; } diff --git a/packages/openapi-generator/src/knownImports.ts b/packages/openapi-generator/src/knownImports.ts index fab630d0..b4025247 100644 --- a/packages/openapi-generator/src/knownImports.ts +++ b/packages/openapi-generator/src/knownImports.ts @@ -49,6 +49,7 @@ export const KNOWN_IMPORTS: KnownImports = { 'io-ts': { string: () => E.right({ type: 'string', primitive: true }), number: () => E.right({ type: 'number', primitive: true }), + integer: () => E.right({ type: 'integer', primitive: true }), bigint: () => E.right({ type: 'number' }), boolean: () => E.right({ type: 'boolean', primitive: true }), null: () => E.right({ type: 'null' }), diff --git a/packages/openapi-generator/test/openapi/base.test.ts b/packages/openapi-generator/test/openapi/base.test.ts index 6617df93..f92e3677 100644 --- a/packages/openapi-generator/test/openapi/base.test.ts +++ b/packages/openapi-generator/test/openapi/base.test.ts @@ -716,6 +716,128 @@ testCase('route with array union of null and undefined', ROUTE_WITH_ARRAY_UNION_ } }); +const ROUTE_WITH_INTEGER_QUERY_PARAM = ` +import * as t from 'io-ts'; +import * as h from '@api-ts/io-ts-http'; + +/** + * A route with integer query parameter + * + * @operationId api.v1.integerTest + * @tag Test Routes + */ +export const route = h.httpRoute({ + path: '/items', + method: 'GET', + request: h.httpRequest({ + query: { + /** + * Maximum number of items to return + * @example 100 + */ + limit: t.integer, + + /** + * Page number for pagination + * @minimum 1 + */ + page: t.integer, + + /** + * Regular number parameter for comparison + */ + ratio: t.number + }, + }), + response: { + 200: t.type({ + items: t.array(t.string), + totalCount: t.integer + }) + }, +}); +`; + +testCase('route with integer query parameters', ROUTE_WITH_INTEGER_QUERY_PARAM, { + openapi: '3.0.3', + info: { + title: 'Test', + version: '1.0.0', + }, + paths: { + '/items': { + get: { + summary: 'A route with integer query parameter', + operationId: 'api.v1.integerTest', + tags: [ + 'Test Routes' + ], + parameters: [ + { + name: 'limit', + description: 'Maximum number of items to return', + in: 'query', + required: true, + schema: { + type: 'integer', + example: 100 + } + }, + { + name: 'page', + description: 'Page number for pagination', + in: 'query', + required: true, + schema: { + type: 'integer', + minimum: 1 + } + }, + { + name: 'ratio', + description: 'Regular number parameter for comparison', + in: 'query', + required: true, + schema: { + type: 'number' + } + } + ], + responses: { + '200': { + description: 'OK', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + items: { + type: 'array', + items: { + type: 'string' + } + }, + totalCount: { + type: 'integer' + } + }, + required: [ + 'items', + 'totalCount' + ] + } + } + } + } + } + } + } + }, + components: { + schemas: {} + } +}); + const MULTIPLE_ROUTES = ` import * as t from 'io-ts'; import * as h from '@api-ts/io-ts-http';