Skip to content

Commit b97760f

Browse files
feat: dynamic content types via JSDoc @ContentType tag
1 parent e19d3ef commit b97760f

File tree

2 files changed

+145
-2
lines changed

2 files changed

+145
-2
lines changed

packages/openapi-generator/src/openapi.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,7 @@ function routeToOpenAPI(route: Route): [string, string, OpenAPIV3.OperationObjec
318318
const isInternal = jsdoc.tags?.private !== undefined;
319319
const isUnstable = jsdoc.tags?.unstable !== undefined;
320320
const example = jsdoc.tags?.example;
321+
const contentType = jsdoc.tags?.contentType ?? 'application/json';
321322

322323
const knownTags = new Set([
323324
'operationId',
@@ -328,6 +329,7 @@ function routeToOpenAPI(route: Route): [string, string, OpenAPIV3.OperationObjec
328329
'tag',
329330
'description',
330331
'url',
332+
'contentType',
331333
]);
332334
const unknownTagsObject = Object.entries(jsdoc.tags ?? {}).reduce(
333335
(acc, [key, value]) => {
@@ -345,7 +347,7 @@ function routeToOpenAPI(route: Route): [string, string, OpenAPIV3.OperationObjec
345347
: {
346348
requestBody: {
347349
content: {
348-
'application/json': { schema: schemaToOpenAPI(route.body) },
350+
[contentType]: { schema: schemaToOpenAPI(route.body) },
349351
},
350352
},
351353
};
@@ -397,7 +399,7 @@ function routeToOpenAPI(route: Route): [string, string, OpenAPIV3.OperationObjec
397399
[Number(code)]: {
398400
description,
399401
content: {
400-
'application/json': {
402+
[contentType]: {
401403
schema: schemaToOpenAPI(response),
402404
...(example !== undefined ? { example } : undefined),
403405
},

packages/openapi-generator/test/openapi/misc.test.ts

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,3 +287,144 @@ testCase('route with record types', ROUTE_WITH_RECORD_TYPES, {
287287
},
288288
},
289289
});
290+
291+
const CONTENT_TYPE_TEST = `
292+
import * as t from 'io-ts';
293+
import * as h from '@api-ts/io-ts-http';
294+
295+
/**
296+
* Route with multipart/form-data content type
297+
*
298+
* @contentType multipart/form-data
299+
* @operationId api.v1.uploadDocument
300+
* @tag Document Upload
301+
*/
302+
export const uploadRoute = h.httpRoute({
303+
path: '/upload',
304+
method: 'POST',
305+
request: h.httpRequest({
306+
body: t.type({
307+
file: t.unknown,
308+
documentType: t.string,
309+
}),
310+
}),
311+
response: {
312+
201: t.type({
313+
id: t.string,
314+
success: t.boolean,
315+
}),
316+
},
317+
});
318+
319+
/**
320+
* Route with default application/json content type
321+
*
322+
* @operationId api.v1.createUser
323+
* @tag User Management
324+
*/
325+
export const createUserRoute = h.httpRoute({
326+
path: '/users',
327+
method: 'POST',
328+
request: h.httpRequest({
329+
body: t.type({
330+
name: t.string,
331+
email: t.string,
332+
}),
333+
}),
334+
response: {
335+
201: t.type({
336+
id: t.string,
337+
name: t.string,
338+
}),
339+
},
340+
});
341+
`;
342+
343+
testCase('route with contentType tag uses multipart/form-data', CONTENT_TYPE_TEST, {
344+
openapi: '3.0.3',
345+
info: {
346+
title: 'Test',
347+
version: '1.0.0',
348+
},
349+
paths: {
350+
'/upload': {
351+
post: {
352+
summary: 'Route with multipart/form-data content type',
353+
operationId: 'api.v1.uploadDocument',
354+
tags: ['Document Upload'],
355+
parameters: [],
356+
requestBody: {
357+
content: {
358+
'multipart/form-data': {
359+
schema: {
360+
type: 'object',
361+
properties: {
362+
file: {},
363+
documentType: { type: 'string' },
364+
},
365+
required: ['file', 'documentType'],
366+
},
367+
},
368+
},
369+
},
370+
responses: {
371+
201: {
372+
description: 'Created',
373+
content: {
374+
'multipart/form-data': {
375+
schema: {
376+
type: 'object',
377+
properties: {
378+
id: { type: 'string' },
379+
success: { type: 'boolean' },
380+
},
381+
required: ['id', 'success'],
382+
},
383+
},
384+
},
385+
},
386+
},
387+
},
388+
},
389+
'/users': {
390+
post: {
391+
summary: 'Route with default application/json content type',
392+
operationId: 'api.v1.createUser',
393+
tags: ['User Management'],
394+
parameters: [],
395+
requestBody: {
396+
content: {
397+
'application/json': {
398+
schema: {
399+
type: 'object',
400+
properties: {
401+
name: { type: 'string' },
402+
email: { type: 'string' },
403+
},
404+
required: ['name', 'email'],
405+
},
406+
},
407+
},
408+
},
409+
responses: {
410+
201: {
411+
description: 'Created',
412+
content: {
413+
'application/json': {
414+
schema: {
415+
type: 'object',
416+
properties: {
417+
id: { type: 'string' },
418+
name: { type: 'string' },
419+
},
420+
required: ['id', 'name'],
421+
},
422+
},
423+
},
424+
},
425+
},
426+
},
427+
},
428+
},
429+
components: { schemas: {} },
430+
});

0 commit comments

Comments
 (0)