Repository: RobinTail/express-zod-api
Pull request #875 (2023-03-12 03:13:17)

Feat: OpenAPI composition
`composition: "components"` for extracting schemas depiction into a dedicated section using references.

Alternative to #711 


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b9073c0b..3ca9f6a3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,11 +2,38 @@
 
 ## Version 9
 
+### v9.4.0
+
+- Feature #875, proposed by [@VideoSystemsTech](https://github.com/VideoSystemsTech).
+  - Ability to document the API specification keeping the schemas organized within named components.
+  - `OpenAPI::constructor()` is equipped with a new optional property `composition` that can be:
+    - `inline` (default) — schemas are depicted directly in a place of their usage;
+    - `components` (feature) — schemas are depicted within the `components` section and have references by their names.
+
+```ts
+// example usage
+new OpenAPI({
+  routing,
+  config,
+  version: "1.2.3",
+  title: "My API",
+  serverUrl: "https://example.com",
+  composition: "components", // <——
+}).getSpecAsYaml();
+```
+
 ### v9.3.1
 
 - Hotfix for the feature #856
   - `$ref` is equipped with the required prefix: `#/components/schemas/`.
 
+```yaml
+before:
+  $ref: 2048581c137c5b2130eb860e3ae37da196dfc25b
+after:
+  $ref: "#/components/schemas/2048581c137c5b2130eb860e3ae37da196dfc25b"
+```
+
 ### v9.3.0
 
 - Feature #856, proposed by [@TheWisestOne](https://github.com/TheWisestOne) in discussion #801.
diff --git a/README.md b/README.md
index ae157e13..7ada8ebb 100644
--- a/README.md
+++ b/README.md
@@ -839,6 +839,7 @@ const yamlString = new OpenAPI({
   version: "1.2.3",
   title: "Example API",
   serverUrl: "https://example.com",
+  composition: "inline", // optional, or "components" for keeping schemas in a separate dedicated section using refs
 }).getSpecAsYaml();
 ```
 
diff --git a/example/example.swagger.yaml b/example/example.swagger.yaml
index 61a3af43..450a03df 100644
--- a/example/example.swagger.yaml
+++ b/example/example.swagger.yaml
@@ -84,10 +84,11 @@ paths:
         - name: id
           in: query
           required: true
+          description: GET /v1/user/retrieve parameter
           schema:
-            description: a numeric string containing the id of the user
             type: string
             pattern: /\d+/
+            description: a numeric string containing the id of the user
   "/v1/user/{id}":
     post:
       operationId: PostV1UserId
@@ -165,17 +166,17 @@ paths:
         - name: id
           in: path
           required: true
+          description: POST /v1/user/:id parameter
           schema:
-            description: POST /v1/user/:id parameter
             type: string
           examples:
             example1:
               value: "12"
       requestBody:
+        description: POST /v1/user/:id request body
         content:
           application/json:
             schema:
-              description: POST /v1/user/:id request body
               allOf:
                 - type: object
                   properties:
@@ -237,8 +238,8 @@ paths:
         - name: userId
           in: query
           required: true
+          description: GET /v1/avatar/send parameter
           schema:
-            description: GET /v1/avatar/send parameter
             type: string
             pattern: /\d+/
   /v1/avatar/stream:
@@ -266,8 +267,8 @@ paths:
         - name: userId
           in: query
           required: true
+          description: GET /v1/avatar/stream parameter
           schema:
-            description: GET /v1/avatar/stream parameter
             type: string
             pattern: /\d+/
   /v1/avatar/upload:
@@ -347,10 +348,10 @@ paths:
       tags:
         - files
       requestBody:
+        description: POST /v1/avatar/upload request body
         content:
           multipart/form-data:
             schema:
-              description: POST /v1/avatar/upload request body
               type: object
               properties:
                 avatar:
diff --git a/src/open-api-helpers.ts b/src/open-api-helpers.ts
index de78c31a..2e6a978a 100644
--- a/src/open-api-helpers.ts
+++ b/src/open-api-helpers.ts
@@ -24,6 +24,7 @@ import {
   getRoutePathParams,
   hasCoercion,
   hasTopLevelTransformingEffect,
+  makeCleanId,
   routePathParamsRegex,
   tryToTransform,
 } from "./common-helpers";
@@ -72,6 +73,8 @@ interface ReqResDepictHelperCommonProps
   method: Method;
   path: string;
   endpoint: AbstractEndpoint;
+  composition: "inline" | "components";
+  clue?: string;
 }
 
 const shortDescriptionLimit = 50;
@@ -613,6 +616,8 @@ export const depictRequestParams = ({
   serializer,
   hasRef,
   makeRef,
+  composition,
+  clue = "parameter",
 }: ReqResDepictHelperCommonProps & {
   inputSources: InputSource[];
 }): ParameterObject[] => {
@@ -625,25 +630,30 @@ export const depictRequestParams = ({
     isParamsEnabled && pathParams.includes(name);
   return Object.keys(shape)
     .filter((name) => isQueryEnabled || isPathParam(name))
-    .map((name) => ({
-      name,
-      in: isPathParam(name) ? "path" : "query",
-      required: !shape[name].isOptional(),
-      schema: {
-        description: `${method.toUpperCase()} ${path} parameter`,
-        ...walkSchema({
-          schema: shape[name],
-          isResponse: false,
-          rules: depicters,
-          onEach,
-          onMissing,
-          serializer,
-          hasRef,
-          makeRef,
-        }),
-      },
-      ...depictParamExamples(schema, false, name),
-    }));
+    .map((name) => {
+      const depicted = walkSchema({
+        schema: shape[name],
+        isResponse: false,
+        rules: depicters,
+        onEach,
+        onMissing,
+        serializer,
+        hasRef,
+        makeRef,
+      });
+      const result =
+        composition === "components"
+          ? makeRef(makeCleanId(path, method, `${clue} ${name}`), depicted)
+          : depicted;
+      return {
+        name,
+        in: isPathParam(name) ? "path" : "query",
+        required: !shape[name].isOptional(),
+        description: `${method.toUpperCase()} ${path} ${clue}`,
+        schema: result,
+        ...depictParamExamples(schema, false, name),
+      };
+    });
 };
 
 export const depicters: HandlingRules<
@@ -751,14 +761,14 @@ export const excludeExampleFromDepiction = (
 export const depictResponse = ({
   method,
   path,
-  description,
   endpoint,
   isPositive,
   serializer,
   hasRef,
   makeRef,
+  composition,
+  clue = "response",
 }: ReqResDepictHelperCommonProps & {
-  description: string;
   isPositive: boolean;
 }): ResponseObject => {
   const schema = endpoint.getSchema(isPositive ? "positive" : "negative");
@@ -776,16 +786,17 @@ export const depictResponse = ({
     })
   );
   const examples = depictExamples(schema, true);
+  const result =
+    composition === "components"
+      ? makeRef(makeCleanId(path, method, clue), depictedSchema)
+      : depictedSchema;
 
   return {
-    description: `${method.toUpperCase()} ${path} ${description}`,
+    description: `${method.toUpperCase()} ${path} ${clue}`,
     content: mimeTypes.reduce(
       (carry, mimeType) => ({
         ...carry,
-        [mimeType]: {
-          schema: depictedSchema,
-          ...examples,
-        },
+        [mimeType]: { schema: result, ...examples },
       }),
       {} as ContentObject
     ),
@@ -892,6 +903,8 @@ export const depictRequest = ({
   serializer,
   hasRef,
   makeRef,
+  composition,
+  clue = "request body",
 }: ReqResDepictHelperCommonProps): RequestBodyObject => {
   const pathParams = getRoutePathParams(path);
   const bodyDepiction = excludeExampleFromDepiction(
@@ -914,18 +927,17 @@ export const depictRequest = ({
     false,
     pathParams
   );
+  const result =
+    composition === "components"
+      ? makeRef(makeCleanId(path, method, clue), bodyDepiction)
+      : bodyDepiction;
 
   return {
+    description: `${method.toUpperCase()} ${path} ${clue}`,
     content: endpoint.getMimeTypes("input").reduce(
       (carry, mimeType) => ({
         ...carry,
-        [mimeType]: {
-          schema: {
-            description: `${method.toUpperCase()} ${path} request body`,
-            ...bodyDepiction,
-          },
-          ...bodyExamples,
-        },
+        [mimeType]: { schema: result, ...bodyExamples },
       }),
       {} as ContentObject
     ),
diff --git a/src/open-api.ts b/src/open-api.ts
index 8e29b3c9..a1a69109 100644
--- a/src/open-api.ts
+++ b/src/open-api.ts
@@ -37,6 +37,8 @@ interface GeneratorParams {
   errorResponseDescription?: string;
   /** @default true */
   hasSummaryFromDescription?: boolean;
+  /** @default inline */
+  composition?: "inline" | "components";
   /**
    * @desc Used for comparing schemas wrapped into z.lazy() to limit the recursion
    * @default JSON.stringify() + SHA1 hash as a hex digest
@@ -97,6 +99,7 @@ export class OpenAPI extends OpenApiBuilder {
     successfulResponseDescription = "Successful response",
     errorResponseDescription = "Error response",
     hasSummaryFromDescription = true,
+    composition = "inline",
     serializer = defaultSerializer,
   }: GeneratorParams) {
     super();
@@ -111,6 +114,7 @@ export class OpenAPI extends OpenApiBuilder {
         path,
         method,
         endpoint,
+        composition,
         serializer,
         hasRef: this.hasRef.bind(this),
         makeRef: this.makeRef.bind(this),
@@ -129,12 +133,12 @@ export class OpenAPI extends OpenApiBuilder {
         responses: {
           [endpoint.getStatusCode("positive")]: depictResponse({
             ...commonParams,
-            description: successfulResponseDescription,
+            clue: successfulResponseDescription,
             isPositive: true,
           }),
           [endpoint.getStatusCode("negative")]: depictResponse({
             ...commonParams,
-            description: errorResponseDescription,
+            clue: errorResponseDescription,
             isPositive: false,
           }),
         },


Test patch:
diff --git a/tests/unit/__snapshots__/open-api-helpers.spec.ts.snap b/tests/unit/__snapshots__/open-api-helpers.spec.ts.snap
index bd5016c8..fd46cd11 100644
--- a/tests/unit/__snapshots__/open-api-helpers.spec.ts.snap
+++ b/tests/unit/__snapshots__/open-api-helpers.spec.ts.snap
@@ -666,11 +666,11 @@ exports[`Open API helpers depictRequestParams() should depict none if both query
 exports[`Open API helpers depictRequestParams() should depict only path params if query is disabled 1`] = `
 [
   {
+    "description": "GET /v1/user/:id parameter",
     "in": "path",
     "name": "id",
     "required": true,
     "schema": {
-      "description": "GET /v1/user/:id parameter",
       "type": "string",
     },
   },
@@ -680,20 +680,20 @@ exports[`Open API helpers depictRequestParams() should depict only path params i
 exports[`Open API helpers depictRequestParams() should depict query and path params 1`] = `
 [
   {
+    "description": "GET /v1/user/:id parameter",
     "in": "path",
     "name": "id",
     "required": true,
     "schema": {
-      "description": "GET /v1/user/:id parameter",
       "type": "string",
     },
   },
   {
+    "description": "GET /v1/user/:id parameter",
     "in": "query",
     "name": "test",
     "required": true,
     "schema": {
-      "description": "GET /v1/user/:id parameter",
       "type": "boolean",
     },
   },
diff --git a/tests/unit/__snapshots__/open-api.spec.ts.snap b/tests/unit/__snapshots__/open-api.spec.ts.snap
index 1673a514..581795bc 100644
--- a/tests/unit/__snapshots__/open-api.spec.ts.snap
+++ b/tests/unit/__snapshots__/open-api.spec.ts.snap
@@ -65,8 +65,8 @@ paths:
         - name: arr
           in: query
           required: true
+          description: GET /v1/getSomething parameter
           schema:
-            description: GET /v1/getSomething parameter
             type: array
             items:
               type: string
@@ -126,10 +126,10 @@ paths:
                     error:
                       message: Sample error message
       requestBody:
+        description: POST /v1/getSomething request body
         content:
           application/json:
             schema:
-              description: POST /v1/getSomething request body
               type: object
               properties:
                 arr:
@@ -227,10 +227,10 @@ paths:
                     error:
                       message: Sample error message
       requestBody:
+        description: POST /v1/getSomething request body
         content:
           application/json:
             schema:
-              description: POST /v1/getSomething request body
               oneOf:
                 - type: object
                   properties:
@@ -338,10 +338,10 @@ paths:
                     error:
                       message: Sample error message
       requestBody:
+        description: POST /v1/getSomething request body
         content:
           application/json:
             schema:
-              description: POST /v1/getSomething request body
               allOf:
                 - type: object
                   properties:
@@ -450,8 +450,8 @@ paths:
         - name: strNum
           in: query
           required: true
+          description: GET /v1/getSomething parameter
           schema:
-            description: GET /v1/getSomething parameter
             type: string
           examples:
             example1:
@@ -539,10 +539,10 @@ paths:
                     error:
                       message: Sample error message
       requestBody:
+        description: POST /v1/getSomething request body
         content:
           application/json:
             schema:
-              description: POST /v1/getSomething request body
               type: object
               properties:
                 strNum:
@@ -632,8 +632,8 @@ paths:
         - name: strNum
           in: query
           required: true
+          description: GET /v1/getSomething parameter
           schema:
-            description: GET /v1/getSomething parameter
             type: string
             example: "123"
 components:
@@ -720,9 +720,10 @@ paths:
         - name: str
           in: query
           required: true
+          description: GET /v1/getSomething parameter
           schema:
-            description: here is the test
             type: string
+            description: here is the test
 components:
   schemas: {}
   responses: {}
@@ -797,8 +798,8 @@ paths:
         - name: name
           in: path
           required: true
+          description: GET /v1/:name parameter
           schema:
-            description: GET /v1/:name parameter
             oneOf:
               - type: string
                 enum:
@@ -809,8 +810,8 @@ paths:
         - name: other
           in: query
           required: true
+          description: GET /v1/:name parameter
           schema:
-            description: GET /v1/:name parameter
             type: boolean
 components:
   schemas: {}
@@ -886,8 +887,8 @@ paths:
         - name: name
           in: path
           required: true
+          description: POST /v1/:name parameter
           schema:
-            description: POST /v1/:name parameter
             oneOf:
               - type: string
                 enum:
@@ -896,10 +897,10 @@ paths:
                 enum:
                   - Jane
       requestBody:
+        description: POST /v1/:name request body
         content:
           application/json:
             schema:
-              description: POST /v1/:name request body
               type: object
               properties:
                 other:
@@ -1111,14 +1112,14 @@ paths:
         - name: key
           in: query
           required: true
+          description: GET /v1/getSomething parameter
           schema:
-            description: GET /v1/getSomething parameter
             type: string
         - name: str
           in: query
           required: true
+          description: GET /v1/getSomething parameter
           schema:
-            description: GET /v1/getSomething parameter
             type: string
       security:
         - APIKEY_1: []
@@ -1172,10 +1173,10 @@ paths:
                     error:
                       message: Sample error message
       requestBody:
+        description: POST /v1/setSomething request body
         content:
           application/json:
             schema:
-              description: POST /v1/setSomething request body
               allOf:
                 - type: object
                   properties: {}
@@ -1234,10 +1235,10 @@ paths:
                     error:
                       message: Sample error message
       requestBody:
+        description: PUT /v1/updateSomething request body
         content:
           application/json:
             schema:
-              description: PUT /v1/updateSomething request body
               allOf:
                 - type: object
                   properties: {}
@@ -1431,8 +1432,8 @@ paths:
         - name: array
           in: query
           required: true
+          description: GET /v1/getSomething parameter
           schema:
-            description: GET /v1/getSomething parameter
             type: array
             items:
               type: integer
@@ -1446,16 +1447,16 @@ paths:
         - name: unlimited
           in: query
           required: true
+          description: GET /v1/getSomething parameter
           schema:
-            description: GET /v1/getSomething parameter
             type: array
             items:
               type: boolean
         - name: transformer
           in: query
           required: true
+          description: GET /v1/getSomething parameter
           schema:
-            description: GET /v1/getSomething parameter
             type: string
 components:
   schemas: {}
@@ -1556,10 +1557,10 @@ paths:
                     error:
                       message: Sample error message
       requestBody:
+        description: POST /v1/getSomething request body
         content:
           application/json:
             schema:
-              description: POST /v1/getSomething request body
               discriminator:
                 propertyName: type
               oneOf:
@@ -1677,10 +1678,10 @@ paths:
                     error:
                       message: Sample error message
       requestBody:
+        description: POST /v1/getSomething request body
         content:
           application/json:
             schema:
-              description: POST /v1/getSomething request body
               type: object
               properties:
                 intersection:
@@ -1778,28 +1779,28 @@ paths:
         - name: optional
           in: query
           required: false
+          description: GET /v1/getSomething parameter
           schema:
-            description: GET /v1/getSomething parameter
             type: string
         - name: optDefault
           in: query
           required: false
+          description: GET /v1/getSomething parameter
           schema:
-            description: GET /v1/getSomething parameter
             type: string
             default: test
         - name: nullish
           in: query
           required: false
+          description: GET /v1/getSomething parameter
           schema:
-            description: GET /v1/getSomething parameter
             nullable: true
             type: boolean
         - name: nuDefault
           in: query
           required: false
+          description: GET /v1/getSomething parameter
           schema:
-            description: GET /v1/getSomething parameter
             nullable: true
             type: integer
             format: int64
@@ -1890,10 +1891,10 @@ paths:
                     error:
                       message: Sample error message
       requestBody:
+        description: POST /v1/getSomething request body
         content:
           application/json:
             schema:
-              description: POST /v1/getSomething request body
               type: object
               properties:
                 union:
@@ -1944,7 +1945,7 @@ servers:
 "
 `;
 
-exports[`Open API generator generateOpenApi() should generate the correct schema of example routing 1`] = `
+exports[`Open API generator generateOpenApi() should generate the correct schema of example routing 0 1`] = `
 "openapi: 3.0.0
 info:
   title: Example API
@@ -2031,10 +2032,11 @@ paths:
         - name: id
           in: query
           required: true
+          description: GET /v1/user/retrieve parameter
           schema:
-            description: a numeric string containing the id of the user
             type: string
             pattern: /\\d+/
+            description: a numeric string containing the id of the user
   "/v1/user/{id}":
     post:
       operationId: PostV1UserId
@@ -2112,17 +2114,17 @@ paths:
         - name: id
           in: path
           required: true
+          description: POST /v1/user/:id parameter
           schema:
-            description: POST /v1/user/:id parameter
             type: string
           examples:
             example1:
               value: "12"
       requestBody:
+        description: POST /v1/user/:id request body
         content:
           application/json:
             schema:
-              description: POST /v1/user/:id request body
               allOf:
                 - type: object
                   properties:
@@ -2184,8 +2186,8 @@ paths:
         - name: userId
           in: query
           required: true
+          description: GET /v1/avatar/send parameter
           schema:
-            description: GET /v1/avatar/send parameter
             type: string
             pattern: /\\d+/
   /v1/avatar/stream:
@@ -2213,8 +2215,8 @@ paths:
         - name: userId
           in: query
           required: true
+          description: GET /v1/avatar/stream parameter
           schema:
-            description: GET /v1/avatar/stream parameter
             type: string
             pattern: /\\d+/
   /v1/avatar/upload:
@@ -2294,10 +2296,10 @@ paths:
       tags:
         - files
       requestBody:
+        description: POST /v1/avatar/upload request body
         content:
           multipart/form-data:
             schema:
-              description: POST /v1/avatar/upload request body
               type: object
               properties:
                 avatar:
@@ -2345,6 +2347,440 @@ servers:
 "
 `;
 
+exports[`Open API generator generateOpenApi() should generate the correct schema of example routing 1 1`] = `
+"openapi: 3.0.0
+info:
+  title: Example API
+  version: 1.2.3
+paths:
+  /v1/user/retrieve:
+    get:
+      operationId: GetV1UserRetrieve
+      responses:
+        "200":
+          description: GET /v1/user/retrieve Successful response
+          content:
+            application/json:
+              schema:
+                $ref: "#/components/schemas/GetV1UserRetrieveSuccessfulResponse"
+        "400":
+          description: GET /v1/user/retrieve Error response
+          content:
+            application/json:
+              schema:
+                $ref: "#/components/schemas/GetV1UserRetrieveErrorResponse"
+              examples:
+                example1:
+                  value:
+                    status: error
+                    error:
+                      message: Sample error message
+      description: Example user retrieval endpoint.
+      summary: Retrieves the user.
+      tags:
+        - users
+      parameters:
+        - name: id
+          in: query
+          required: true
+          description: GET /v1/user/retrieve parameter
+          schema:
+            $ref: "#/components/schemas/GetV1UserRetrieveParameterId"
+  "/v1/user/{id}":
+    post:
+      operationId: PostV1UserId
+      responses:
+        "200":
+          description: POST /v1/user/:id Successful response
+          content:
+            application/json:
+              schema:
+                $ref: "#/components/schemas/PostV1UserIdSuccessfulResponse"
+              examples:
+                example1:
+                  value:
+                    status: success
+                    data:
+                      name: John Doe
+                      createdAt: 2021-12-31T00:00:00.000Z
+        "400":
+          description: POST /v1/user/:id Error response
+          content:
+            application/json:
+              schema:
+                $ref: "#/components/schemas/PostV1UserIdErrorResponse"
+              examples:
+                example1:
+                  value:
+                    status: error
+                    error:
+                      message: Sample error message
+      description: Changes the user record. Example user update endpoint.
+      summary: Changes the user record. Example user update endp…
+      tags:
+        - users
+      parameters:
+        - name: id
+          in: path
+          required: true
+          description: POST /v1/user/:id parameter
+          schema:
+            $ref: "#/components/schemas/PostV1UserIdParameterId"
+          examples:
+            example1:
+              value: "12"
+      requestBody:
+        description: POST /v1/user/:id request body
+        content:
+          application/json:
+            schema:
+              $ref: "#/components/schemas/PostV1UserIdRequestBody"
+            examples:
+              example1:
+                value:
+                  key: 1234-5678-90
+                  name: John Doe
+                  birthday: 1963-04-21
+      security:
+        - APIKEY_1: []
+          APIKEY_2: []
+  /v1/avatar/send:
+    get:
+      operationId: GetV1AvatarSend
+      responses:
+        "200":
+          description: GET /v1/avatar/send Successful response
+          content:
+            image/svg+xml:
+              schema:
+                $ref: "#/components/schemas/GetV1AvatarSendSuccessfulResponse"
+        "400":
+          description: GET /v1/avatar/send Error response
+          content:
+            text/plain:
+              schema:
+                $ref: "#/components/schemas/GetV1AvatarSendErrorResponse"
+      summary: Sends a file content.
+      tags:
+        - files
+        - users
+      parameters:
+        - name: userId
+          in: query
+          required: true
+          description: GET /v1/avatar/send parameter
+          schema:
+            $ref: "#/components/schemas/GetV1AvatarSendParameterUserid"
+  /v1/avatar/stream:
+    get:
+      operationId: GetV1AvatarStream
+      responses:
+        "200":
+          description: GET /v1/avatar/stream Successful response
+          content:
+            image/*:
+              schema:
+                $ref: "#/components/schemas/GetV1AvatarStreamSuccessfulResponse"
+        "400":
+          description: GET /v1/avatar/stream Error response
+          content:
+            text/plain:
+              schema:
+                $ref: "#/components/schemas/GetV1AvatarStreamErrorResponse"
+      summary: Streams a file content.
+      tags:
+        - users
+        - files
+      parameters:
+        - name: userId
+          in: query
+          required: true
+          description: GET /v1/avatar/stream parameter
+          schema:
+            $ref: "#/components/schemas/GetV1AvatarStreamParameterUserid"
+  /v1/avatar/upload:
+    post:
+      operationId: PostV1AvatarUpload
+      responses:
+        "200":
+          description: POST /v1/avatar/upload Successful response
+          content:
+            application/json:
+              schema:
+                $ref: "#/components/schemas/PostV1AvatarUploadSuccessfulResponse"
+        "400":
+          description: POST /v1/avatar/upload Error response
+          content:
+            application/json:
+              schema:
+                $ref: "#/components/schemas/PostV1AvatarUploadErrorResponse"
+              examples:
+                example1:
+                  value:
+                    status: error
+                    error:
+                      message: Sample error message
+      description: Handles a file upload.
+      summary: Handles a file upload.
+      tags:
+        - files
+      requestBody:
+        description: POST /v1/avatar/upload request body
+        content:
+          multipart/form-data:
+            schema:
+              $ref: "#/components/schemas/PostV1AvatarUploadRequestBody"
+components:
+  schemas:
+    GetV1UserRetrieveParameterId:
+      type: string
+      pattern: /\\d+/
+      description: a numeric string containing the id of the user
+    2048581c137c5b2130eb860e3ae37da196dfc25b:
+      type: array
+      items:
+        type: object
+        properties:
+          title:
+            type: string
+          features:
+            $ref: "#/components/schemas/2048581c137c5b2130eb860e3ae37da196dfc25b"
+        required:
+          - title
+          - features
+    GetV1UserRetrieveSuccessfulResponse:
+      type: object
+      properties:
+        status:
+          type: string
+          enum:
+            - success
+        data:
+          type: object
+          properties:
+            id:
+              type: integer
+              format: int64
+              minimum: 0
+              exclusiveMinimum: false
+              maximum: 9007199254740991
+              exclusiveMaximum: false
+            name:
+              type: string
+            features:
+              type: array
+              items:
+                type: object
+                properties:
+                  title:
+                    type: string
+                  features:
+                    $ref: "#/components/schemas/2048581c137c5b2130eb860e3ae37da196dfc25b"
+                required:
+                  - title
+                  - features
+          required:
+            - id
+            - name
+            - features
+      required:
+        - status
+        - data
+    GetV1UserRetrieveErrorResponse:
+      type: object
+      properties:
+        status:
+          type: string
+          enum:
+            - error
+        error:
+          type: object
+          properties:
+            message:
+              type: string
+          required:
+            - message
+      required:
+        - status
+        - error
+    PostV1UserIdParameterId:
+      type: string
+    PostV1UserIdSuccessfulResponse:
+      type: object
+      properties:
+        status:
+          type: string
+          enum:
+            - success
+        data:
+          type: object
+          properties:
+            name:
+              type: string
+            createdAt:
+              description: YYYY-MM-DDTHH:mm:ss.sssZ
+              type: string
+              format: date-time
+              externalDocs:
+                url: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString
+          required:
+            - name
+            - createdAt
+          example:
+            name: John Doe
+            createdAt: 2021-12-31T00:00:00.000Z
+      required:
+        - status
+        - data
+    PostV1UserIdErrorResponse:
+      type: object
+      properties:
+        status:
+          type: string
+          enum:
+            - error
+        error:
+          type: object
+          properties:
+            message:
+              type: string
+          required:
+            - message
+      required:
+        - status
+        - error
+    PostV1UserIdRequestBody:
+      allOf:
+        - type: object
+          properties:
+            key:
+              type: string
+              minLength: 1
+          required:
+            - key
+          example:
+            key: 1234-5678-90
+        - type: object
+          properties:
+            name:
+              type: string
+              minLength: 1
+            birthday:
+              description: YYYY-MM-DDTHH:mm:ss.sssZ
+              type: string
+              format: date-time
+              pattern: ^\\d{4}-\\d{2}-\\d{2}(T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?)?Z?$
+              externalDocs:
+                url: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString
+          required:
+            - name
+            - birthday
+          example:
+            name: John Doe
+            birthday: 1963-04-21
+    GetV1AvatarSendParameterUserid:
+      type: string
+      pattern: /\\d+/
+    GetV1AvatarSendSuccessfulResponse:
+      type: string
+    GetV1AvatarSendErrorResponse:
+      type: string
+    GetV1AvatarStreamParameterUserid:
+      type: string
+      pattern: /\\d+/
+    GetV1AvatarStreamSuccessfulResponse:
+      type: string
+      format: binary
+    GetV1AvatarStreamErrorResponse:
+      type: string
+    PostV1AvatarUploadSuccessfulResponse:
+      type: object
+      properties:
+        status:
+          type: string
+          enum:
+            - success
+        data:
+          type: object
+          properties:
+            name:
+              type: string
+            size:
+              type: integer
+              format: int64
+              minimum: 0
+              exclusiveMinimum: false
+              maximum: 9007199254740991
+              exclusiveMaximum: false
+            mime:
+              type: string
+            hash:
+              type: string
+            otherInputs:
+              type: object
+              additionalProperties:
+                format: any
+                nullable: true
+          required:
+            - name
+            - size
+            - mime
+            - hash
+            - otherInputs
+      required:
+        - status
+        - data
+    PostV1AvatarUploadErrorResponse:
+      type: object
+      properties:
+        status:
+          type: string
+          enum:
+            - error
+        error:
+          type: object
+          properties:
+            message:
+              type: string
+          required:
+            - message
+      required:
+        - status
+        - error
+    PostV1AvatarUploadRequestBody:
+      type: object
+      properties:
+        avatar:
+          type: string
+          format: binary
+      required:
+        - avatar
+  responses: {}
+  parameters: {}
+  examples: {}
+  requestBodies: {}
+  headers: {}
+  securitySchemes:
+    APIKEY_1:
+      type: apiKey
+      in: query
+      name: key
+    APIKEY_2:
+      type: apiKey
+      in: header
+      name: token
+  links: {}
+  callbacks: {}
+tags:
+  - name: users
+    description: Everything about the users
+  - name: files
+    description: Everything about the files processing
+servers:
+  - url: http://example.com
+"
+`;
+
 exports[`Open API generator generateOpenApi() should handle bigint, boolean, date and null 1`] = `
 "openapi: 3.0.0
 info:
@@ -2413,10 +2849,10 @@ paths:
                     error:
                       message: Sample error message
       requestBody:
+        description: POST /v1/getSomething request body
         content:
           application/json:
             schema:
-              description: POST /v1/getSomething request body
               type: object
               properties:
                 bigint:
@@ -2518,10 +2954,10 @@ paths:
                     error:
                       message: Sample error message
       requestBody:
+        description: POST /v1/getSomething request body
         content:
           application/json:
             schema:
-              description: POST /v1/getSomething request body
               type: object
               properties:
                 name:
@@ -2674,10 +3110,10 @@ paths:
                     error:
                       message: Sample error message
       requestBody:
+        description: POST /v1/getSomething request body
         content:
           application/json:
             schema:
-              description: POST /v1/getSomething request body
               type: object
               properties:
                 double:
@@ -2828,10 +3264,10 @@ paths:
                     error:
                       message: Sample error message
       requestBody:
+        description: POST /v1/getSomething request body
         content:
           application/json:
             schema:
-              description: POST /v1/getSomething request body
               type: object
               properties:
                 regular:
@@ -2972,10 +3408,10 @@ paths:
                     error:
                       message: Sample error message
       requestBody:
+        description: POST /v1/getSomething request body
         content:
           application/json:
             schema:
-              description: POST /v1/getSomething request body
               type: object
               properties:
                 regularEnum:
@@ -3107,10 +3543,10 @@ paths:
                     error:
                       message: Sample error message
       requestBody:
+        description: POST /v1/getSomething request body
         content:
           application/json:
             schema:
-              description: POST /v1/getSomething request body
               type: object
               properties: {}
 components:
@@ -3188,10 +3624,10 @@ paths:
                     error:
                       message: Sample error message
       requestBody:
+        description: POST /v1/getSomething request body
         content:
           application/json:
             schema:
-              description: POST /v1/getSomething request body
               type: object
               properties:
                 one:
@@ -3286,10 +3722,10 @@ paths:
                     error:
                       message: Sample error message
       requestBody:
+        description: POST /v1/getSomething request body
         content:
           application/json:
             schema:
-              description: POST /v1/getSomething request body
               type: object
               properties:
                 ofOne:
@@ -3409,8 +3845,8 @@ paths:
         - name: any
           in: query
           required: false
+          description: GET /v1/getSomething parameter
           schema:
-            description: GET /v1/getSomething parameter
             format: any
             nullable: true
 components:
@@ -3490,15 +3926,15 @@ paths:
         - name: string
           in: query
           required: false
+          description: GET /v1/getSomething parameter
           schema:
-            description: GET /v1/getSomething parameter
             format: string (preprocessed)
             nullable: true
         - name: number
           in: query
           required: true
+          description: GET /v1/getSomething parameter
           schema:
-            description: GET /v1/getSomething parameter
             format: int64 (preprocessed)
             minimum: 0
             exclusiveMinimum: false
diff --git a/tests/unit/open-api-helpers.spec.ts b/tests/unit/open-api-helpers.spec.ts
index b03a59e7..5b5d13c0 100644
--- a/tests/unit/open-api-helpers.spec.ts
+++ b/tests/unit/open-api-helpers.spec.ts
@@ -745,6 +745,7 @@ describe("Open API helpers", () => {
             handler: jest.fn(),
           }),
           inputSources: ["query", "params"],
+          composition: "inline",
           hasRef: hasRefMock,
           makeRef: makeRefMock,
           serializer: defaultSerializer,
@@ -767,6 +768,7 @@ describe("Open API helpers", () => {
             handler: jest.fn(),
           }),
           inputSources: ["body", "params"],
+          composition: "inline",
           hasRef: hasRefMock,
           makeRef: makeRefMock,
           serializer: defaultSerializer,
@@ -789,6 +791,7 @@ describe("Open API helpers", () => {
             handler: jest.fn(),
           }),
           inputSources: ["body"],
+          composition: "inline",
           hasRef: hasRefMock,
           makeRef: makeRefMock,
           serializer: defaultSerializer,
diff --git a/tests/unit/open-api.spec.ts b/tests/unit/open-api.spec.ts
index a1b5323f..2f53d9da 100644
--- a/tests/unit/open-api.spec.ts
+++ b/tests/unit/open-api.spec.ts
@@ -23,16 +23,23 @@ describe("Open API generator", () => {
   });
 
   describe("generateOpenApi()", () => {
-    test("should generate the correct schema of example routing", () => {
-      const spec = new OpenAPI({
-        routing,
-        config: exampleConfig,
-        version: "1.2.3",
-        title: "Example API",
-        serverUrl: "http://example.com",
-      }).getSpecAsYaml();
-      expect(spec).toMatchSnapshot();
-    });
+    test.each([
+      { composition: "inline" },
+      { composition: "components" },
+    ] as const)(
+      "should generate the correct schema of example routing %#",
+      ({ composition }) => {
+        const spec = new OpenAPI({
+          routing,
+          config: exampleConfig,
+          version: "1.2.3",
+          title: "Example API",
+          serverUrl: "http://example.com",
+          composition,
+        }).getSpecAsYaml();
+        expect(spec).toMatchSnapshot();
+      }
+    );
 
     test("should generate the correct schema for DELETE request without body", () => {
       const spec = new OpenAPI({