Skip to content

Commit 006f920

Browse files
authored
fix: support array notation in @response and @Body annotations (Type[]) (#55)
1 parent 7b08c23 commit 006f920

File tree

6 files changed

+293
-67
lines changed

6 files changed

+293
-67
lines changed

examples/next15-app-drizzle-zod/public/openapi.json

Lines changed: 53 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,10 @@
7272
"content": {
7373
"application/json": {
7474
"schema": {
75-
"$ref": "#/components/schemas/PostResponseSchema[]"
75+
"type": "array",
76+
"items": {
77+
"$ref": "#/components/schemas/PostResponseSchema"
78+
}
7679
}
7780
}
7881
}
@@ -303,7 +306,55 @@
303306
}
304307
}
305308
},
306-
"PostResponseSchema[]": {},
309+
"PostResponseSchema": {
310+
"type": "object",
311+
"properties": {
312+
"title": {
313+
"type": "string",
314+
"description": "Post title"
315+
},
316+
"slug": {
317+
"type": "string",
318+
"description": "URL-friendly slug"
319+
},
320+
"excerpt": {
321+
"type": "string",
322+
"description": "Post excerpt"
323+
},
324+
"content": {
325+
"type": "string",
326+
"description": "Full post content"
327+
},
328+
"published": {
329+
"type": "boolean",
330+
"description": "Publication status"
331+
},
332+
"viewCount": {
333+
"type": "integer",
334+
"description": "Number of views"
335+
},
336+
"createdAt": {
337+
"type": "string",
338+
"format": "date-time",
339+
"description": "Creation timestamp"
340+
},
341+
"updatedAt": {
342+
"type": "string",
343+
"format": "date-time",
344+
"description": "Last update timestamp"
345+
}
346+
},
347+
"required": [
348+
"title",
349+
"slug",
350+
"excerpt",
351+
"content",
352+
"published",
353+
"viewCount",
354+
"createdAt",
355+
"updatedAt"
356+
]
357+
},
307358
"CreatePostSchema": {
308359
"type": "object",
309360
"properties": {
@@ -361,55 +412,6 @@
361412
"id"
362413
]
363414
},
364-
"PostResponseSchema": {
365-
"type": "object",
366-
"properties": {
367-
"title": {
368-
"type": "string",
369-
"description": "Post title"
370-
},
371-
"slug": {
372-
"type": "string",
373-
"description": "URL-friendly slug"
374-
},
375-
"excerpt": {
376-
"type": "string",
377-
"description": "Post excerpt"
378-
},
379-
"content": {
380-
"type": "string",
381-
"description": "Full post content"
382-
},
383-
"published": {
384-
"type": "boolean",
385-
"description": "Publication status"
386-
},
387-
"viewCount": {
388-
"type": "integer",
389-
"description": "Number of views"
390-
},
391-
"createdAt": {
392-
"type": "string",
393-
"format": "date-time",
394-
"description": "Creation timestamp"
395-
},
396-
"updatedAt": {
397-
"type": "string",
398-
"format": "date-time",
399-
"description": "Last update timestamp"
400-
}
401-
},
402-
"required": [
403-
"title",
404-
"slug",
405-
"excerpt",
406-
"content",
407-
"published",
408-
"viewCount",
409-
"createdAt",
410-
"updatedAt"
411-
]
412-
},
413415
"UpdatePostSchema": {
414416
"type": "object",
415417
"properties": {

examples/next15-app-zod/public/openapi.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2326,7 +2326,7 @@
23262326
"/users-paginated": {
23272327
"get": {
23282328
"operationId": "get-users-paginated",
2329-
"summary": "Get paginated users",
2329+
"summary": "Get paginated users\r",
23302330
"description": "Retrieve users with cursor-based pagination using factory-generated schema",
23312331
"tags": [
23322332
"Users-paginated"

src/lib/route-processor.ts

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,16 +48,42 @@ export class RouteProcessor {
4848
const successCode =
4949
dataTypes.successCode || this.getDefaultSuccessCode(method);
5050
if (dataTypes.responseType) {
51-
// Ensure the schema is defined in components/schemas
51+
// Handle array notation (e.g., "Type[]", "Type[][]", "Generic<T>[]")
52+
let schema: any;
53+
let baseType = dataTypes.responseType;
54+
let arrayDepth = 0;
55+
56+
// Count and remove array brackets
57+
while (baseType.endsWith('[]')) {
58+
arrayDepth++;
59+
baseType = baseType.slice(0, -2);
60+
}
61+
62+
// Ensure the base schema is defined in components/schemas
5263
this.schemaProcessor.getSchemaContent({
53-
responseType: dataTypes.responseType,
64+
responseType: baseType,
5465
});
5566

67+
// Build schema reference
68+
if (arrayDepth === 0) {
69+
// Not an array
70+
schema = { $ref: `#/components/schemas/${baseType}` };
71+
} else {
72+
// Build nested array schema
73+
schema = { $ref: `#/components/schemas/${baseType}` };
74+
for (let i = 0; i < arrayDepth; i++) {
75+
schema = {
76+
type: "array",
77+
items: schema,
78+
};
79+
}
80+
}
81+
5682
responses[successCode] = {
5783
description: dataTypes.responseDescription || "Successful response",
5884
content: {
5985
"application/json": {
60-
schema: { $ref: `#/components/schemas/${dataTypes.responseType}` },
86+
schema: schema,
6187
},
6288
},
6389
};

src/lib/schema-processor.ts

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1059,12 +1059,26 @@ export class SchemaProcessor {
10591059
body: OpenAPIDefinition;
10601060
responses: OpenAPIDefinition;
10611061
} {
1062+
// Helper function to strip array notation from type names
1063+
const stripArrayNotation = (typeName: string | undefined): string | undefined => {
1064+
if (!typeName) return typeName;
1065+
let baseType = typeName;
1066+
while (baseType.endsWith('[]')) {
1067+
baseType = baseType.slice(0, -2);
1068+
}
1069+
return baseType;
1070+
};
1071+
1072+
// Strip array notation for schema lookups
1073+
const baseBodyType = stripArrayNotation(bodyType);
1074+
const baseResponseType = stripArrayNotation(responseType);
1075+
10621076
let params = paramsType ? this.openapiDefinitions[paramsType] : {};
10631077
let pathParams = pathParamsType
10641078
? this.openapiDefinitions[pathParamsType]
10651079
: {};
1066-
let body = bodyType ? this.openapiDefinitions[bodyType] : {};
1067-
let responses = responseType ? this.openapiDefinitions[responseType] : {};
1080+
let body = baseBodyType ? this.openapiDefinitions[baseBodyType] : {};
1081+
let responses = baseResponseType ? this.openapiDefinitions[baseResponseType] : {};
10681082

10691083
if (paramsType && !params) {
10701084
this.findSchemaDefinition(paramsType, "params");
@@ -1076,22 +1090,22 @@ export class SchemaProcessor {
10761090
pathParams = this.openapiDefinitions[pathParamsType] || {};
10771091
}
10781092

1079-
if (bodyType && !body) {
1080-
this.findSchemaDefinition(bodyType, "body");
1081-
body = this.openapiDefinitions[bodyType] || {};
1093+
if (baseBodyType && !body) {
1094+
this.findSchemaDefinition(baseBodyType, "body");
1095+
body = this.openapiDefinitions[baseBodyType] || {};
10821096
}
10831097

1084-
if (responseType && !responses) {
1085-
this.findSchemaDefinition(responseType, "response");
1086-
responses = this.openapiDefinitions[responseType] || {};
1098+
if (baseResponseType && !responses) {
1099+
this.findSchemaDefinition(baseResponseType, "response");
1100+
responses = this.openapiDefinitions[baseResponseType] || {};
10871101
}
10881102

10891103
if (this.schemaTypes.includes("zod")) {
10901104
const schemasToProcess = [
10911105
paramsType,
10921106
pathParamsType,
1093-
bodyType,
1094-
responseType,
1107+
baseBodyType,
1108+
baseResponseType,
10951109
].filter(Boolean);
10961110
schemasToProcess.forEach((schemaName) => {
10971111
if (!this.openapiDefinitions[schemaName]) {

src/lib/utils.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -193,10 +193,10 @@ export function extractTypeFromComment(
193193
commentValue: string,
194194
tag: string
195195
): string {
196-
// Updated regex to support generic types with angle brackets
196+
// Updated regex to support generic types with angle brackets and array brackets
197197
return (
198198
commentValue
199-
.match(new RegExp(`${tag}\\s*\\s*([\\w<>,\\s]+)`))?.[1]
199+
.match(new RegExp(`${tag}\\s*\\s*([\\w<>,\\s\\[\\]]+)`))?.[1]
200200
?.trim() || ""
201201
);
202202
}

0 commit comments

Comments
 (0)