OpenAPIでRestfulAPIの仕様を定義する
一時期会社の他チームで、OpenAPIを取り入れようと検討されていた時期があった。結局他の開発業務が立て込んだため採用はされなかったのだが、API開発の一手法として興味があったのでちょっと使ってみた。
OpenAPIとは
OpenAPIは、RestfulAPIのインターフェースを、人間とコンピュータ両方によって読みやすい形で定義するための一手法だ。YAML形式またはJSON形式で定義を行う。
様々な言語やフレームワークによって、OpenAPIの定義ファイルをコードに変換するプラグインが用意されていることが多い。そのため、仮にソフトウェアの言語・フレームワークを変更することになっても、定義ファイルを共通で使うことができれば開発を効率化できる。また、定義ファイルをもとにドキュメントも自動生成できてしまう。
OpenAPIを書いてみる
ちょっとしたTodoアプリのAPIを定義してみた。なお、今回使っているのはOpenAPI v3.0の方だ。
paths
基本的には、 paths
の子要素としてエンドポイントを定義し、それぞれのエンドポイントごとにリクエストパラメータ・リクエストボディ・レスポンス等を書いていけば良い。例えば、Todoアプリのタスク一覧をGETで取得するエンドポイントは以下のように定義する。
/tasks: get: summary: List all tasks operationId: listTasks responses: '200': description: array of tasks content: application/json: schema: $ref: '#/components/schemas/Tasks' default: description: unexpected error content: application/json: schema: $ref: '#/components/schemas/Error'
また、タスクのIDを指定して編集を行うエンドポイントはこのように定義できる。 リクエストパラメータは paramter
以下で定義したうえで、エンドポイントの中で {}
で囲んで使用する。
/tasks/{taskId}: put: summary: edit a task operationId: editTask parameters: - name: taskId # taskIdパラメータの定義 in: path required: true description: id of task schema: type: string requestBody: description: task to edit content: application/json: schema: $ref: '#/components/schemas/TaskRequest' responses: '201': description: edited content: application/json: schema: $ref: '#/components/schemas/Task' default: description: unexpected error content: application/json: schema: $ref: '#/components/schemas/Error'
components
各エンドポイントのリクエストボディ・レスポンスの定義は、それぞれのpathオブジェクト内の content
内で行える。しかし、共通して使いそうなschemaが多い場合は、 components/schema
以下で定義できる。
先程の例では、$ref
というキーの中で、 Tasks
, Task
, TaskRequest
, Error
というschemaが指定されていた。このようにすることで、components/schema以下で定義したオブジェクトを参照できる。
例えば、追加・編集したタスクを返すレスポンス用に、以下のようなschemaを定義した。
components: schemas: Task: description: one task object type: object allOf: - $ref: '#/components/schemas/TaskRequest' - required: - id - done properties: id: type: integer format: int64 done: type: boolean example: id: 1 content: Buy notebook urgent: 3 important: 3 done: false
required
以下では必須のパラメータを定義している。また、 properties
以下では各パラメータの詳細を記載している。
allOf
という指定があるが、これは「子要素のschema全てを満たす」という意味だ。この場合、 TaskRequest
schemaで定義されたフィールドに加え、 required
指定となっている id
と done
のフィールドも持たなければいけない。
ちなみに、 TaskRequest
schemaは以下のように書いている。
TaskRequest: description: object to create or edit a new task type: object required: - content - urgent - important properties: content: type: string urgent: type: integer format: int64 important: type: integer format: int64 example: content: Buy notebook urgent: 3 important: 3
これらを分けた理由は、新しくタスクを作る場合や編集する場合に、 id
と done
を指定させたくなかったためだ。 id
はタスク作成の際にデータベース側で付与され、編集の際もリクエストパラメータで指定する。また、 done
についてはタスクを完了にするための別のエンドポイント( /tasks/{taskId}/done
)に切り出したいと考えた。このように、一部のフィールドが重なる複数のリクエストを使いたいとき、 allOf
などでschemaを結合させるとうまく書ける。
example
example以下に記載した内容は、ドキュメントを生成した際の、各リクエスト・レスポンスの例として使われる。例が書かれていないドキュメントは非常に分かりにくいので、適切なexampleを記載することも重要だ。
exampleを書ける場所はいくつかある。
- paths以下の各エンドポイント内にある
content
内で定義 components/schema
以下の各schemaの子要素として定義components/examples
以下に定義
どれを使うかは、examplesをどれほど共通化したいかによる。ただ実際に書いてみて思ったのだが、例を共通化しすぎるとかえって煩雑になるときが結構ありそうだ。例えば、1つのpathの中に場合分けして複数の例を書きたい場合や、同じschemaでもpathごとに使う文脈が異なるため例を分けたいという場合がある。例は様々なパターンをカバーしている方がユーザにとって分かりやすくなるので、あまり共通化に囚われすぎない方が良いかもしれない。