はじめに
ユニットテストとかを書くためにAPIのリクエストとレスポンスがJSON形式で欲しいとなったときに、今まではOpenAPIドキュメントをReDocで表示してそこに載っているサンプルをコピー&JSON形式で保存し直すみたいなことをしていました。
ただしこの方法は、APIの数が増えてきてかつ変更もちょくちょくあるような場合だと結構面倒な作業ですし、CIにも組み込みにくいです。
わざわざこの画面を経由しないでもOpenAPIドキュメントのyamlからこのrequestBody/responsesだけを直接JSONで吐き出す方法は何か無いものかと色々調べてみたのですが、元のOpenAPIドキュメントをyaml ⇆ JSONで変換する類の話しか見つからなかったので簡単なスクリプトを書くことにしました。
(もしかしたら自分の調査不足なだけで実は既にこの手のことをローカルでやってくれるツールはあるのかもしれません...)
JSON出力スクリプト
やりたいこととしては以下のような感じです
- コマンドライン引数でOpenAPI (Swagger) 形式のyamlファイルを渡すとそのファイル内の全APIのrequestBody/responsesをJSON形式で出力する
- GETなどrequestBodyが無い場合は空のJSONを出力する
- responsesはステータスコード毎にJSONを出力する
理想はReDocのコピーボタンをクリックしたときにクリップボードに保存されるのと同じような形式のJSONを取得することだったので、当該処理がどうなっているのかソースコードをちょっと読んでみたところ、openapi-samplerというのを使ってそうだということが分かりました。
今回はそれを利用するためにスクリプトはNode.jsで書いています。
完成したスクリプトはこんな感じです。
package.jsonとか含めたコードはGitHubで公開しています。
https://github.com/NatsuToku/openapi-sample-json-generator
// ファイル出力の設定
const inputFile = process.argv.length == 3 ? process.argv[2] : "openapi.yaml";
const outputBasePath = "./output";
const outputRequestJSONName = "request";
const outputResponseJSONPrefix = "response";
const JSONSpaceNum = 4;
// パース方法の設定
const mediaType = "application/json";
const skipNonRequired = false;
const skipReadOnly = true;
const skipWriteOnly = false;
// モジュールのimport
const SwaggerParser = require("@apidevtools/swagger-parser");
const OpenAPISampler = require("openapi-sampler");
const fs = require("fs");
(async () => {
// $refポインタを含まないOpenAPI定義のオブジェクトを取得する
const parser = await SwaggerParser.dereference(inputFile);
// APIのパス毎に処理する
Object.keys(parser.paths).forEach(function (path) {
// 同じパスのメソッド毎に処理する
Object.keys(parser.paths[path]).forEach(function (method) {
// ファイル出力用のパスをAPIのパスとメソッドに基づいて設定する
// ex) パスが /a/b/c でメソッドが POST -> /outputBasePath/a_b_c/post
const outputPath = `${outputBasePath}/${path
.replace("/", "")
.replace(/\//g, "_")}/${method}`;
// outputBasePath内にファイル出力用のディレクトリを作成する
fs.mkdir(outputPath, { recursive: true }, (err) => {
const api = parser.paths[path][method];
// requestBodyが存在している場合はサンプルJSONオブジェクトを生成する
let requestSample = {};
if (
api.hasOwnProperty("requestBody") &&
api.requestBody.hasOwnProperty(mediaType)
) {
requestSample = OpenAPISampler.sample(
api.requestBody.content[mediaType].schema,
{
skipNonRequired: skipNonRequired,
skipReadOnly: skipReadOnly,
skipWriteOnly: skipWriteOnly,
}
);
}
// requestBodyのJSONを出力する (requestBodyが存在しない場合は空)
fs.writeFileSync(
`${outputPath}/${outputRequestJSONName}.json`,
JSON.stringify(requestSample, null, JSONSpaceNum)
);
// ステータスコード毎に処理する
const responses = api.responses;
Object.keys(responses).forEach(function (status) {
// responseのcontentが存在している場合はサンプルJSONオブジェクトを生成する
let responseSample = {};
if (
responses[status].hasOwnProperty("content") &&
responses[status].content.hasOwnProperty(mediaType)
) {
responseSample = OpenAPISampler.sample(
responses[status].content[mediaType].schema,
{
skipNonRequired: skipNonRequired,
skipReadOnly: skipReadOnly,
skipWriteOnly: skipWriteOnly,
}
);
}
// responsesのJSONを出力する (responseのcontentが存在しない場合は空)
fs.writeFileSync(
`${outputPath}/${outputResponseJSONPrefix}_${status}.json`,
JSON.stringify(responseSample, null, JSONSpaceNum)
);
});
});
});
});
})();
実行結果
以下のコマンドで実行します。
node ./generator.js <OPENAPI_FILE_NAME>
引数のファイル名は省略するとデフォルトでは「./openapi.yaml」を読むようにしています。
yaml形式でしかテストしてませんがswagger-parserが対応しているのでおそらくJSON形式でも大丈夫だと思われます。
例えば以下のようなyamlを読ませた場合(内容は適当です)
openapi: 3.0.0
info:
title: "Book API"
version: "1.0"
servers:
- url: "https://xxxxxx.com"
paths:
/book:
get:
summary: Get book list
description: "本の一覧を取得する"
responses:
200:
description: "本の一覧"
content:
application/json:
schema:
type: "array"
items:
$ref: "#/components/schemas/BookIndex"
400:
description: Bad Request
content:
application/json:
schema:
$ref: "#/components/schemas/BadRequest"
post:
summary: Create book
description: "本の情報を新規登録する"
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/Book"
responses:
200:
description: "OK"
content:
application/json:
schema:
$ref: "#/components/schemas/BookIndex"
400:
description: Bad Request
content:
application/json:
schema:
$ref: "#/components/schemas/BadRequest"
/book/{id}:
get:
summary: Get book detail
description: "本の詳細を取得する"
parameters:
- name: id
in: path
description: "unique key"
required: true
schema:
type: integer
responses:
200:
description: "本の一覧"
content:
application/json:
schema:
$ref: "#/components/schemas/BookIndex"
400:
description: Bad Request
content:
application/json:
schema:
$ref: "#/components/schemas/BadRequest"
delete:
summary: Delete book
description: "本の登録を削除する"
parameters:
- name: id
in: path
description: "unique key"
required: true
schema:
type: integer
responses:
200:
description: OK
400:
description: Bad Request
content:
application/json:
schema:
$ref: "#/components/schemas/BadRequest"
components:
schemas:
Author:
type: object
required:
- name
properties:
name:
type: string
example: "test"
age:
type: number
example: 30
gender:
type: string
example: "unknown"
Book:
type: object
required:
- title
- price
- authors
properties:
title:
type: string
example: "book title"
price:
type: number
example: 1500
authors:
type: array
items:
$ref: "#/components/schemas/Author"
category:
type: "string"
description: "book category"
example: "horror"
BookIndex:
allOf:
- type: object
properties:
id:
type: integer
description: "unique key"
- $ref: "#/components/schemas/Book"
BadRequest:
type: object
required:
- message
properties:
message:
type: string
example: "error"
こんな感じに出力されます
output
├──book
│ ├── get
│ │ ├── request.json
│ │ ├── response_200.json
│ │ └── response_400.json
│ └─── post
│ ├── request.json
│ ├── response_200.json
│ └── response_400.json
└──book_{id}
├── delete
│ ├── request.json
│ ├── response_200.json
│ └── response_400.json
└─── get
├── request.json
├── response_200.json
└── response_400.json
例えば/bookのPOSTのresponse_200.jsonの中身はこうなってます
{
"id": 0,
"title": "book title",
"price": 1500,
"authors": [
{
"name": "test",
"age": 30,
"gender": "unknown"
}
],
"category": "horror"
}
コード内容の説明
// ファイル出力の設定
const inputFile = process.argv.length == 3 ? process.argv[2] : "openapi.yaml";
const outputBasePath = "./output";
const outputRequestJSONName = "request";
const outputResponseJSONPrefix = "response";
const JSONSpaceNum = 4;
// パース方法の設定
const mediaType = "application/json";
const skipNonRequired = false;
const skipReadOnly = true;
const skipWriteOnly = false;
以下の内容を設定しています。
-
inputFile
: OpenAPI (Swagger) 形式のファイル。コマンドライン引数で指定。なければデフォルト「openaip.yaml」 -
outputBasePath
:JSONファイルを出力するディレクトリ -
outputRequestJSONName
:requestBodyのJSONを出力するときのファイル名。この場合だと「request.json」 -
outputResponseJSONPrefix
:responsesのcontentのJSONを出力するときのファイル名のプレフィックス。この場合だと「response_<status_code>.json」 -
JSONSpaceNum
:出力するJSONファイルのインデントのスペース数 -
mediaType
:OpenAPIをパースするときに対象とするメディアタイプ。"application/json"固定で良さそう -
skipNonRequired
:required
がtrueのパラメータをスキップする(JSONで出力しない)かどうか -
skipReadOnly
:readOnly
がtrueのパラメータをスキップする(JSONで出力しない)かどうか -
skipWriteOnly
:writeOnly
がtrueのパラメータをスキップする(JSONで出力しない)かどうか
// モジュールのimport
const SwaggerParser = require("@apidevtools/swagger-parser");
const OpenAPISampler = require("openapi-sampler");
const fs = require("fs");
-
@apidevtools/swagger-parser
- OpenAPI (Swagger) 形式のファイルを渡すとrefの解決をした状態のオブジェクトを返してくれる
- 素でyamlファイルを開いてしまうとrefもそのままになってしまうので使用
-
openapi-sampler
- OpenAPI Schemaオブジェクトを渡すとサンプルのJSONオブジェクトを返してくれる
- ReDocでは内部的にこれを利用している模様
-
fs
- ファイル関係の操作で使用
(async () => {
// $refポインタを含まないOpenAPI定義のオブジェクトを取得する
const parser = await SwaggerParser.dereference(inputFile);
swagger-parserがawaitな関数なのでasync functionで囲んだ上で入力ファイルから $refポインタを含まないOpenAPI定義のオブジェクトを取得しています。
// APIのパス毎に処理する
Object.keys(parser.paths).forEach(function (path) {
// 同じパスのメソッド毎に処理する
Object.keys(parser.paths[path]).forEach(function (method) {
OpenAPIのpathsの中の各メソッド毎に処理します。
このforEach内でさらにforEatchを回す書き方はなんとなくもっと良い書き方がある気がしています...
// ファイル出力用のパスをAPIのパスとメソッドに基づいて設定する
// ex) パスが /a/b/c でメソッドが POST -> /outputBasePath/a_b_c/post
const outputPath = `${outputBasePath}/${path
.replace("/", "")
.replace(/\//g, "_")}/${method}`;
JSONファイルを出力するパスは、OpenAPIのpathsとmethodで決めています。
pathはそのままだと「/a/b/c
」みたいな形式なので、最初の「/」は消して、残りの「/」は全て「_」に置換することで「 a_b_c
」みたいなディレクトリになり、さらにその下にgetやpostと言ったディレクトリを作ります。
// outputBasePath内にファイル出力用のディレクトリを作成する
fs.mkdir(outputPath, { recursive: true }, (err) => {
const api = parser.paths[path][method];
先ほど定義したoutputPathのディレクトリを作成します。
recursiveをtrueにすることで再帰的に作成。
既にディレクトリがあるとerrになるのですが、ここではerrが発生してもそのまま握り潰してます。
(のでerr変数はこの後使われない)
// requestBodyが存在している場合はサンプルJSONオブジェクトを生成する
let requestSample = {};
if (
api.hasOwnProperty("requestBody") &&
api.requestBody.hasOwnProperty(mediaType)
) {
requestSample = OpenAPISampler.sample(
api.requestBody.content[mediaType].schema,
{
skipNonRequired: skipNonRequired,
skipReadOnly: skipReadOnly,
skipWriteOnly: skipWriteOnly,
}
);
}
// requestBodyのJSONを出力する (requestBodyが存在しない場合は空)
fs.writeFileSync(
`${outputPath}/${outputResponseJSONPrefix}_${status}.json`,
JSON.stringify(requestSample, null, JSONSpaceNum)
);
apiにrequestBodyが含まれかつmediaTypeがapplication/jsonだったらopenapi-samplerでサンプルのオブジェクトを作成し、その後そのオブジェクトをJSONファイルとして出力します。
GETのようにrequestBodyが無いような場合は空のJSONを出力します。
(出力したくなければwriteFileSyncの前に条件判断を入れれば良さそう)
// ステータスコード毎に処理する
const responses = api.responses;
Object.keys(responses).forEach(function (status) {
// responseのcontentが存在している場合はサンプルJSONオブジェクトを生成する
let responseSample = {};
if (
responses[status].hasOwnProperty("content") &&
responses[status].content.hasOwnProperty(mediaType)
) {
responseSample = OpenAPISampler.sample(
responses[status].content[mediaType].schema,
{
skipNonRequired: skipNonRequired,
skipReadOnly: skipReadOnly,
skipWriteOnly: skipWriteOnly,
}
);
}
// responsesのJSONを出力する (responseのcontentが存在しない場合は空)
fs.writeFileSync(
`${outputPath}/response_${status}.json`,
JSON.stringify(responseSample, null, JSONSpaceNum)
requestBodyと同様にcontentが含まれかつmediaTypeがapplication/jsonだったらopenapi-samplerでサンプルのオブジェクトを作成し、その後そのオブジェクトをJSONファイルとして出力します。
requestBodyとの違いとしてはresponsesはステータスコード毎に複数設定可能であるため、処理もステータスコードをforEatchで回しています。
また、出力するファイル名のサフィックスにステータスコードをくっつけるようにしています。
最後に
普段Node.jsはあまり書いてない+こういうスクリプト的なものは初めて書いたのでコマンドライン引数の取り方とかファイルの保存方法とか個人的には勉強になりました。
Node.jsの書き方のベストプラクティス的なことを把握しないでフィーリングで書いている部分が多いので、ここの書き方微妙だよ的な指摘があればバンバンしていただけるとありがたいです。
参考
OpenAPI Specification - Version 3.0.3 | Swagger
GitHub - Redocly/openapi-sampler: Tool for generation samples based on OpenAPI(fka Swagger) payload/response schema
Swagger 2.0 and OpenAPI 3.0 parser/validator | Swagger Parser
Node.jsでコマンドライン引数を取得する - Qiita
[Node.js]ディレクトリの作成と削除をする
How can I pretty-print JSON using node.js? - Stack Overflow