Skip to content

Commit 261797f

Browse files
Support multiple responses for a given status code
1 parent 34818b1 commit 261797f

File tree

2 files changed

+74
-16
lines changed

2 files changed

+74
-16
lines changed

src/common/openapi.ts

Lines changed: 62 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { OpenApiGeneratorV31, OpenAPIRegistry, RouteConfig } from "@asteasolutions/zod-to-openapi";
2-
import { AnyZodObject, ZodType } from "zod";
3-
import type { InfoObject, OpenAPIObject, ServerObject } from "openapi3-ts/oas31";
2+
import { AnyZodObject, z, ZodType } from "zod";
3+
import type { ExampleObject, InfoObject, OpenAPIObject, ServerObject } from "openapi3-ts/oas31";
44
import Config from "./config";
55
import { ResponsesObject, Specification } from "../middleware/specification";
66
import { SwaggerUiOptions } from "swagger-ui-express";
@@ -60,6 +60,65 @@ export function getOpenAPISpec(): OpenAPIObject {
6060
return openAPISpec;
6161
}
6262

63+
// Given a specification, returns the route responses in openapi format
64+
function getPathResponsesForSpecification<
65+
Params extends AnyZodObject,
66+
Query extends AnyZodObject,
67+
Responses extends ResponsesObject,
68+
Body extends ZodType,
69+
>(specification: Specification<Params, Query, Responses, Body>): RouteConfig["responses"] {
70+
const responses: RouteConfig["responses"] = {};
71+
72+
for (const [statusCode, response] of Object.entries(specification.responses)) {
73+
// response can be a single response or an array of responses for this status code
74+
// First, check for the easy singular case
75+
if (!Array.isArray(response)) {
76+
const { description, schema } = response;
77+
responses[statusCode] = {
78+
description,
79+
content: {
80+
"application/json": {
81+
schema,
82+
},
83+
},
84+
};
85+
} else {
86+
// Otherwise, we need to combine these multiple responses for the same status code into a singular entry
87+
const description =
88+
"One of:\n" +
89+
response.map((r) => `- ${r.id}: ${r.description}`).join("\n") +
90+
"\n\n**See examples dropdown below**";
91+
const schemas = response.map((r) => r.schema) as [ZodType, ZodType, ...ZodType[]];
92+
const examples = response.reduce<Record<string, ExampleObject>>((acc, r) => {
93+
const example = r.schema._def.openapi?.metadata?.example;
94+
if (example) {
95+
if (acc[r.id]) {
96+
throw Error(
97+
`Duplicate definition of response id ${r.id} for ${specification.method} ${specification.path} status ${statusCode}`,
98+
);
99+
}
100+
acc[r.id] = {
101+
description: r.description,
102+
value: example,
103+
};
104+
}
105+
return acc;
106+
}, {});
107+
responses[statusCode] = {
108+
description,
109+
content: {
110+
"application/json": {
111+
schema: z.union(schemas),
112+
examples,
113+
},
114+
},
115+
};
116+
}
117+
}
118+
119+
return responses;
120+
}
121+
63122
export function registerPathSpecification<
64123
Params extends AnyZodObject,
65124
Query extends AnyZodObject,
@@ -83,17 +142,7 @@ export function registerPathSpecification<
83142
combinedDescription = descriptionHeader || description;
84143
}
85144

86-
const responses: RouteConfig["responses"] = {};
87-
for (const [statusCode, response] of Object.entries(specification.responses)) {
88-
responses[statusCode] = {
89-
description: response.description,
90-
content: {
91-
"application/json": {
92-
schema: response.schema,
93-
},
94-
},
95-
};
96-
}
145+
const responses = getPathResponsesForSpecification(specification);
97146

98147
const request: RouteConfig["request"] = { params, query };
99148
if (specification.body) {

src/middleware/specification.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,10 @@ export interface ResponseObject {
3232
description: string;
3333
schema: ZodType;
3434
}
35+
type ResponseObjectWithId = ResponseObject & { id: string };
36+
export type ResponseObjectsWithId = [ResponseObjectWithId, ResponseObjectWithId, ...ResponseObjectWithId[]];
3537
export interface ResponsesObject {
36-
[statusCode: string]: ResponseObject;
38+
[statusCode: string]: ResponseObject | ResponseObjectsWithId;
3739
}
3840

3941
export interface Specification<Params = ZodUnknown, Query = ZodUnknown, Responses = ResponsesObject, Body = ZodUnknown> {
@@ -50,10 +52,17 @@ export interface Specification<Params = ZodUnknown, Query = ZodUnknown, Response
5052
}
5153

5254
// Utility types to convert Responses into a set of possible schemas
53-
type InferResponseBody<T> = T extends ResponseObject ? z.infer<T["schema"]> : never;
55+
// This type takes in a ResponseObject or ResponseObjectsWithId and returns the underlying inferred zod types
56+
type InferResponseBody<T> = T extends ResponseObject
57+
? z.infer<T["schema"]>
58+
: T extends ResponseObjectsWithId
59+
? z.infer<T[number]["schema"]>
60+
: never;
61+
62+
// This type indexes each possible key in the ResponsesObject and passes it to InferResponseBody to get the underlying types
5463
type ResponseBody<T extends ResponsesObject> = InferResponseBody<T[keyof T]>;
5564

56-
// Utility type for a zod object which is really just empty
65+
// Utility type for a zod object which is really just empty (not just {} in ts)
5766
type ZodEmptyObject = ZodObject<NonNullable<unknown>>;
5867

5968
export default function specification<

0 commit comments

Comments
 (0)