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 指定となっている iddone のフィールドも持たなければいけない。
ちなみに、 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

これらを分けた理由は、新しくタスクを作る場合や編集する場合に、 iddone を指定させたくなかったためだ。 id はタスク作成の際にデータベース側で付与され、編集の際もリクエストパラメータで指定する。また、 done についてはタスクを完了にするための別のエンドポイント( /tasks/{taskId}/done )に切り出したいと考えた。このように、一部のフィールドが重なる複数のリクエストを使いたいとき、 allOf などでschemaを結合させるとうまく書ける。

example

example以下に記載した内容は、ドキュメントを生成した際の、各リクエスト・レスポンスの例として使われる。例が書かれていないドキュメントは非常に分かりにくいので、適切なexampleを記載することも重要だ。
exampleを書ける場所はいくつかある。

  • paths以下の各エンドポイント内にある content 内で定義
  • components/schema 以下の各schemaの子要素として定義
  • components/examples 以下に定義

どれを使うかは、examplesをどれほど共通化したいかによる。ただ実際に書いてみて思ったのだが、例を共通化しすぎるとかえって煩雑になるときが結構ありそうだ。例えば、1つのpathの中に場合分けして複数の例を書きたい場合や、同じschemaでもpathごとに使う文脈が異なるため例を分けたいという場合がある。例は様々なパターンをカバーしている方がユーザにとって分かりやすくなるので、あまり共通化に囚われすぎない方が良いかもしれない。