適当にJSON Serverで作成したMockからOpenAPIの定義を生成できないか試したのでメモ。
結果としては一応生成できたが余分な値や足りない(クエリパラメータなど)部分がある。

ディレクトリ構成

./gen-openapi-from-json-server
|-- db.json
|-- package-lock.json
|-- package.json
`-- server.js

package.json

express-oas-generatorを使用して生成している。ただしそのままではschemaやexamplesが生成されないため、
localhostにリクエストしキャプチャして生成するために node-fetch を入れた。

package.json
{
  "name": "gen-openapi-from-json-server",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "gen": "node server.js"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "express-oas-generator": "^1.0.45",
    "json-server": "^0.17.0",
    "node-fetch": "^2.6.7"
  }
}

db.json

json-serverで使用しているdb.json

db.json
{
  "pet": [
    {
      "id": 10,
      "name": "doggie",
      "category": {
        "id": 1,
        "name": "Dogs"
      },
      "photoUrls": [
        "string"
      ],
      "tags": [
        {
          "id": 0,
          "name": "string"
        }
      ],
      "status": "available"
    }
  ],
  "user": [
    {
      "id": 10,
      "username": "theUser",
      "firstName": "John",
      "lastName": "James",
      "email": "[email protected]",
      "password": "12345",
      "phone": "12345",
      "userStatus": 1
    }
  ]
}

server.js

今回適当に作成した生成スクリプト。
express-oas-generatorを使用して生成しているが、pathしか生成されないのでキャプチャ機能を用いてdb.jsonの値をリクエストしてキャプチャしている。
クエリパラメータも試しにリクエストしてみたが、キャプチャされないためコメントアウト。
生成後はjson-serverは終了する。

server.js
const express = require("express");
const fs = require("fs");
const fetch = require("node-fetch");
const db = require("./db.json");

const BASE_URL = "http://localhost:3000";

const keys = Object.keys(db);

const postProcess = async (cb) => {
  for (const key of keys) {
    await pluralRoutesRequest(key, db[key][0]);
    // queryパラメータはキャプチャしてくれない
    // await filterRequest(key, value);
  }
  cb();
};

// Plural routes
const pluralRoutesRequest = async (path, data) => {
  // GET    /posts
  let response = await fetch(`${BASE_URL}/${path}`);
  // GET    /posts/1
  response = await fetch(`${BASE_URL}/${path}/${data.id}`);
  // PUT    /posts/1
  response = await fetch(`${BASE_URL}/${path}/${data.id}`, {
    method: "PUT",
    body: JSON.stringify(data),
    headers: { "Content-Type": "application/json" },
  });
  // PATCH  /posts/1
  response = await fetch(`${BASE_URL}/${path}/${data.id}`, {
    method: "PATCH",
    body: JSON.stringify(data),
    headers: { "Content-Type": "application/json" },
  });
  // DELETE /posts/1
  response = await fetch(`${BASE_URL}/${path}/${data.id}`, {
    method: "DELETE",
  });
  // POST   /posts
  response = await fetch(`${BASE_URL}/${path}`, {
    method: "POST",
    body: JSON.stringify(data),
    headers: { "Content-Type": "application/json" },
  });
};

// Filter
const filterRequest = async (path, list) => {
  let query = Object.entries(list[0]).reduce((previous, [key, value]) => {
    if (key === "id") {
      return previous;
    }

    previous = previous ? `${previous}&` : previous;

    return `${previous}${key}=${value}`;
  }, "");

  // GET /posts?title=json-server&author=typicode
  let response = await fetch(`${BASE_URL}/${path}?${query}`);
  // GET /posts?id=1&id=2
  response = await fetch(
    `${BASE_URL}/${path}/?id=${list[0].id}&id=id=${list[1].id}`
  );
};

const expressOasGenerator = require("express-oas-generator");
const jsonServer = require("json-server");
const server = jsonServer.create();

server.use(express.json()); // for parsing application/json
server.use(express.urlencoded({ extended: true })); // for parsing application/x-www-form-urlencoded

// expressOasGenerator.init(server, {});
expressOasGenerator.handleResponses(server, {
  swaggerUiServePath: "api-docs",
  specOutputPath: "./openapi.json",
  predefinedSpec: {},
  writeIntervalMs: 60 * 1000,
  // mongooseModels: ["User", "Student"],
  mongooseModels: [],
  tags: keys,
  ignoredNodeEnvironments: ["production"],
  alwaysServeDocs: true,
  specOutputFileBehavior: "PRESERVE",
  // specOutputFileBehavior: "RECREATE",
  swaggerDocumentOptions: {},
});

const router = jsonServer.router("db.json");
const middlewares = jsonServer.defaults();

server.use(middlewares);
server.use(router);

expressOasGenerator.handleRequests();

const s = server.listen(3000, () => {
  // console.info("JSON Server is running");
  postProcess(() => {
    expressOasGenerator.getSpecV3((err, spec) => {
      // console.log(JSON.stringify(spec));
      const paths = spec.paths;
      delete paths["/db"];
      delete paths["/{resource}/{id}/{nested}"];
      fs.writeFileSync("my-openapi_v3.json", JSON.stringify(spec));
      s.close(() => {
        console.info("Done.");
      });
    });
  });
});

結果

my-openapi_v3.json
{
  "openapi": "3.0.0",
  "info": {
    "title": "workspace",
    "version": "1.0.0",
    "license": { "name": "ISC" },
    "description": "Specification JSONs: [v2](/api-spec/v2), [v3](/api-spec/v3)."
  },
  "paths": {
    "/pet": {
      "get": {
        "summary": "/pet",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "id": { "type": "number" },
                      "name": { "type": "string" },
                      "category": {
                        "type": "object",
                        "properties": {
                          "id": { "type": "number" },
                          "name": { "type": "string" }
                        }
                      },
                      "photoUrls": {
                        "type": "array",
                        "items": { "type": "string" }
                      },
                      "tags": {
                        "type": "array",
                        "items": {
                          "type": "object",
                          "properties": {
                            "id": { "type": "number" },
                            "name": { "type": "string" }
                          }
                        }
                      },
                      "status": { "type": "string" }
                    }
                  }
                }
              }
            }
          }
        },
        "tags": ["pet"]
      },
      "post": {
        "summary": "/pet",
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": { "type": "number", "example": 10 },
                    "name": { "type": "string", "example": "doggie" },
                    "category": {
                      "type": "object",
                      "properties": {
                        "id": { "type": "number", "example": 1 },
                        "name": { "type": "string", "example": "Dogs" }
                      }
                    },
                    "photoUrls": {
                      "type": "array",
                      "items": { "type": "string" },
                      "example": ["string"]
                    },
                    "tags": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": { "type": "number" },
                          "name": { "type": "string" }
                        }
                      },
                      "example": [{ "id": 0, "name": "string" }]
                    },
                    "status": { "type": "string", "example": "available" }
                  }
                }
              }
            }
          }
        },
        "tags": ["pet"]
      }
    },
    "/pet/{id}": {
      "get": {
        "summary": "/pet/{id}",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": { "type": "string" }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": { "type": "number", "example": 10 },
                    "name": { "type": "string", "example": "doggie" },
                    "category": {
                      "type": "object",
                      "properties": {
                        "id": { "type": "number", "example": 1 },
                        "name": { "type": "string", "example": "Dogs" }
                      }
                    },
                    "photoUrls": {
                      "type": "array",
                      "items": { "type": "string" },
                      "example": ["string"]
                    },
                    "tags": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": { "type": "number" },
                          "name": { "type": "string" }
                        }
                      },
                      "example": [{ "id": 0, "name": "string" }]
                    },
                    "status": { "type": "string", "example": "available" }
                  }
                }
              }
            }
          }
        },
        "tags": ["pet"]
      },
      "put": {
        "summary": "/pet/{id}",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": { "type": "string" }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": { "type": "number", "example": 10 },
                    "name": { "type": "string", "example": "doggie" },
                    "category": {
                      "type": "object",
                      "properties": {
                        "id": { "type": "number", "example": 1 },
                        "name": { "type": "string", "example": "Dogs" }
                      }
                    },
                    "photoUrls": {
                      "type": "array",
                      "items": { "type": "string" },
                      "example": ["string"]
                    },
                    "tags": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": { "type": "number" },
                          "name": { "type": "string" }
                        }
                      },
                      "example": [{ "id": 0, "name": "string" }]
                    },
                    "status": { "type": "string", "example": "available" }
                  }
                }
              }
            }
          }
        },
        "tags": ["pet"]
      },
      "patch": {
        "summary": "/pet/{id}",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": { "type": "string" }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": { "type": "number", "example": 10 },
                    "name": { "type": "string", "example": "doggie" },
                    "category": {
                      "type": "object",
                      "properties": {
                        "id": { "type": "number", "example": 1 },
                        "name": { "type": "string", "example": "Dogs" }
                      }
                    },
                    "photoUrls": {
                      "type": "array",
                      "items": { "type": "string" },
                      "example": ["string"]
                    },
                    "tags": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": { "type": "number" },
                          "name": { "type": "string" }
                        }
                      },
                      "example": [{ "id": 0, "name": "string" }]
                    },
                    "status": { "type": "string", "example": "available" }
                  }
                }
              }
            }
          }
        },
        "tags": ["pet"]
      },
      "delete": {
        "summary": "/pet/{id}",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": { "type": "string" }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": { "type": "object", "properties": {} }
              }
            }
          }
        },
        "tags": ["pet"]
      }
    },
    "/user": {
      "get": {
        "summary": "/user",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "id": { "type": "number" },
                      "username": { "type": "string" },
                      "firstName": { "type": "string" },
                      "lastName": { "type": "string" },
                      "email": { "type": "string" },
                      "password": { "type": "string" },
                      "phone": { "type": "string" },
                      "userStatus": { "type": "number" }
                    }
                  }
                }
              }
            }
          }
        },
        "tags": ["user"]
      },
      "post": {
        "summary": "/user",
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": { "type": "number", "example": 10 },
                    "username": { "type": "string", "example": "theUser" },
                    "firstName": { "type": "string", "example": "John" },
                    "lastName": { "type": "string", "example": "James" },
                    "email": { "type": "string", "example": "[email protected]" },
                    "password": { "type": "string", "example": "******" },
                    "phone": { "type": "string", "example": "12345" },
                    "userStatus": { "type": "number", "example": 1 }
                  }
                }
              }
            }
          }
        },
        "tags": ["user"]
      }
    },
    "/user/{id}": {
      "get": {
        "summary": "/user/{id}",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": { "type": "string" }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": { "type": "number", "example": 10 },
                    "username": { "type": "string", "example": "theUser" },
                    "firstName": { "type": "string", "example": "John" },
                    "lastName": { "type": "string", "example": "James" },
                    "email": { "type": "string", "example": "[email protected]" },
                    "password": { "type": "string", "example": "******" },
                    "phone": { "type": "string", "example": "12345" },
                    "userStatus": { "type": "number", "example": 1 }
                  }
                }
              }
            }
          }
        },
        "tags": ["user"]
      },
      "put": {
        "summary": "/user/{id}",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": { "type": "string" }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": { "type": "number", "example": 10 },
                    "username": { "type": "string", "example": "theUser" },
                    "firstName": { "type": "string", "example": "John" },
                    "lastName": { "type": "string", "example": "James" },
                    "email": { "type": "string", "example": "[email protected]" },
                    "password": { "type": "string", "example": "******" },
                    "phone": { "type": "string", "example": "12345" },
                    "userStatus": { "type": "number", "example": 1 }
                  }
                }
              }
            }
          }
        },
        "tags": ["user"]
      },
      "patch": {
        "summary": "/user/{id}",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": { "type": "string" }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": { "type": "number", "example": 10 },
                    "username": { "type": "string", "example": "theUser" },
                    "firstName": { "type": "string", "example": "John" },
                    "lastName": { "type": "string", "example": "James" },
                    "email": { "type": "string", "example": "[email protected]" },
                    "password": { "type": "string", "example": "******" },
                    "phone": { "type": "string", "example": "12345" },
                    "userStatus": { "type": "number", "example": 1 }
                  }
                }
              }
            }
          }
        },
        "tags": ["user"]
      },
      "delete": {
        "summary": "/user/{id}",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": { "type": "string" }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": { "type": "object", "properties": {} }
              }
            }
          }
        },
        "tags": ["user"]
      }
    }
  },
  "tags": [{ "name": "pet" }, { "name": "user" }]
}