🔔

OpenAPI定義ファイルを分割して管理し易くする

2023/01/21に公開

はじめに

API仕様書をOpenAPIで定義する際にファイル分割してメンテナンスしやすくしようという話です。

OpenAPI定義はVSCodeで作成していきます。
VSCodeでOpenAPI定義ファイルを作成する際に便利な拡張機能については以下の記事にまとめてあります。
https://zenn.dev/yamatonokuni/articles/bd547c74fe9007

本記事の定義例をVSCodeで表示したプレビューです。

OpenAPI Swagger UIプレビュー

最終的なファイル分割構成

openapi  ................................ Rootディレクトリ 
  │
  ├─components  ......................... Components Objectを格納するディレクトリ
  │  ├─examples  ........................ Example Objectを格納するディレクトリ
  │  │      booking.yaml  ............... Example Objectを定義したファイル
  │  │      error.yaml  ................. Example Objectを定義したファイル 
  │  │
  │  └─schemas  ......................... Schema Objectを格納するディレクトリ
  │          booking.yaml  .............. Schema Objectを定義したファイル
  │          error.yaml  ................ Schema Objectを定義したファイル
  │          hotel.yaml  ................ Schema Objectを定義したファイル
  │
  └─paths  .............................. Path Item Objectを格納するディレクトリ
          hotels.yaml  .................. Path Item Objectを定義したファイル
          reservations.yaml  ............ Path Item Objectを定義したファイル

ファイル分割の仕方

OpenAPI仕様に沿ってファイルを分割していきます。OpenAPIでは、 $ref を使って他のコンポーネントを参照します。
主に、以下のオブジェクトで $ref を使用することができます。

  • paths/{path}
  • components/schemas
  • components/responses
  • components/parameters
  • components/examples
  • components/requestBodies
  • components/headers
  • components/securitySchemes

具体的には、以下のように記載します。

同一ファイルのコンポーネントを参照する場合

$ref の値に参照先コンポーネントのパスを指定します。この時、ルートを # で表します。

    $ref: "#/ABCComponent"

他のファイルのコンポーネントを参照する場合

ファイルそのものがひとつのコンポーネントの場合は、$ref の値にファイルパスを指定します。

    $ref: "./paths/file.yaml"

ファイル内のコンポーネントを参照する場合は、$ref の値に、ファイルパスに続けて参照先コンポーネントのパスを指定します。

    $ref: "./paths/file.yaml#/XYZComponent"

分割前のファイル

分割前のファイルです。今回は例示用に作成したこのファイルを例に分割していきます。
(長すぎる...)

openapi: "3.0.3"
info:
  version: 2023.1.1
  title: XXX Travel
  description: Travel service

servers:
  - url: http://xxx.travel.net/v1
    description: Online Travel Agency Service

tags:
  - name: Hotel
    description: Hotel Operation
  - name: Booking
    description: Booking Operation

paths:
  /hotels:
    get:
      summary: Get all hotels
      operationId: getHotels
      tags:
        - Hotel
      parameters:
        - name: limit
          in: query
          description: How many hotels to return at one time (max 100)
          required: false
          schema:
            type: integer
            maximum: 100
            format: int32
      responses:
        '200':
          description: A paged array of hotels
          content:
            application/json:    
              schema:
                $ref: "#/components/schemas/hotels"
        default:
          description: unexpected error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/error"
              examples:
                unexpected_error:
                  $ref: "#/components/examples/unexpected_error"
    
  /reservations:
    post:
      summary: Make a reservation
      operationId: postReservations
      tags:
        - Booking
      requestBody:
        content:
          'application/json':
            schema:
              $ref: "#/components/schemas/reservation"
            examples:
              reservation:
                $ref: "#/components/examples/reservation"
      responses:
        '201':
          description: Response when reservation is successful.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/reservationInfo"
        default:
          description: Response when reservation fails.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/error"
              examples:
                unexpected_error:
                  $ref: "#/components/examples/unexpected_error"

  /reservations/{reservationId}:
    get:
      summary: Get a specified reservation
      operationId: getReservationsById
      tags:
        - Booking
      parameters:
        - name: reservationId
          in: path
          required: true
          description: The id of the reservation to retrieve
          schema:
            type: integer
            format: int64
      responses:
        '200':
          description: Expected response to a valid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/reservationInfo"
        default:
          description: unexpected error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/error"
              examples:
                unexpected_error:
                  $ref: "#/components/examples/unexpected_error"
    delete:
      summary: Cancel a specified reservation
      operationId: deleteReservationsById
      tags:
        - Booking
      parameters:
        - name: reservationId
          in: path
          required: true
          description: The id of the reservation to cancel
          schema:
            type: integer
            format: int64
      responses:
        '201':
          description: Null response.
        default:
          description: unexpected error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/error"
              examples:
                unexpected_error:
                  $ref: "#/components/examples/unexpected_error"

components:
  schemas:
    hotel:
      type: object
      required:
        - hotelId
        - name
      properties:
        hotelId:
          type: integer
          format: int64
        name:
          type: string
    hotels:
      type: array
      maxItems: 100
      items:
        $ref: "#/components/schemas/hotel"
    reservation:
      type: object
      required:
        - date
        - guest
        - hotel
      properties:
        date:
          type: string
          format: date
        guest:
          type: string
        hotel:
          $ref: "#/components/schemas/hotel"
    reservationInfo:
      type: object
      required:
        - reservationId
        - reservation
      properties:
        reservationId:
          type: integer
          format: int64
        reservation:
          $ref: "#/components/schemas/reservation"
    error:
      type: object
      required:
        - code
        - message
      properties:
        code:
          type: integer
          format: int32
        message:
          type: string

  examples:
    reservation:
      summary: Reservation request
      value: |-
        {
          "date": "2023-01-01",
          "guest": "Foo Bar",
          "hotel": {
            "hotelId": 1,
            "name": "HOGE Hotel"
          }
        }
    overload_error:
      summary: Processing overload
      value: |-
        {
          "code": "ERR90001",
          "message": "We couldn't handle your request."
        }
    unexpected_error:
      summary: Unexpected error
      value: |-
        {
          "code": "ERR90002",
          "message": "We couldn't handle your request."
        }

分割後のファイル

ファイル分割する際は、「Path Item Objectのファイル名はパス名に沿って命名する」など、ファイル名を工夫するとわかりやすいと思います。
また、コンポーネントのファイル分割単位はAPI毎、意味的な集まり毎、オブジェクト毎など、API仕様書の規模やプロジェクトの考え方に応じて管理しやすい単位に分割すれば良いと思います。

それでは、分割したファイルたちを並べていきます。

xxx_travel.yaml

openapi: "3.0.3"
info:
  version: 2023.1.1
  title: XXX Travel
  description: Travel service
servers:
  - url: http://xxx.travel.net/v1
    description: Online Travel Agency Service
tags:
  - name: Hotel
    description: Hotel Operation
  - name: Booking
    description: Booking Operation
paths:
  /hotels:
    $ref: "./paths/hotels.yaml#/hotels"
  /reservations:
    $ref: "./paths/reservations.yaml#/reservations"
  /reservations/{reservationId}:
    $ref: "./paths/reservations.yaml#/reservations_{reservationId}"
components:
  schemas:
    hotel:
      $ref: "./components/schemas/hotel.yaml#/hotel"
    hotels:
      $ref: "./components/schemas/hotel.yaml#/hotels"
    reservation:
      $ref: "./components/schemas/booking.yaml#/reservation"
    reservationInfo:
      $ref: "./components/schemas/booking.yaml#/reservationInfo"
    error:
      $ref: "./components/schemas/error.yaml#/error"

./paths/hotels.yaml

hotels:
  get:
    summary: Get all hotels
    operationId: getHotels
    tags:
      - Hotel
    parameters:
      - name: limit
        in: query
        description: How many hotels to return at one time (max 100)
        required: false
        schema:
          type: integer
          maximum: 100
          format: int32
    responses:
      '200':
        description: A paged array of hotels
        content:
          application/json:    
            schema:
              $ref: "../components/schemas/hotel.yaml#/hotels"
      default:
        description: unexpected error
        content:
          application/json:
            schema:
              $ref: "../components/schemas/error.yaml#/error"
            examples:
              unexpected_error:
                $ref: "../components/examples/error.yaml#/unexpected_error"
              overload_error:
                $ref: "../components/examples/error.yaml#/overload_error"

./paths/reservations.yaml

reservations:
  post:
    summary: Make a reservation
    operationId: postReservations
    tags:
      - Booking
    requestBody:
      content:
        application/json:
          schema:
            $ref: "../components/schemas/booking.yaml#/reservation"
          examples:
            reservation:
              $ref: "../components/examples/booking.yaml#/reservation"
    responses:
      '201':
        description: Response when reservation is successful.
        content:
          application/json:
            schema:
              $ref: "../components/schemas/booking.yaml#/reservationInfo"
      default:
        description: Response when reservation fails.
        content:
          application/json:
            schema:
              $ref: "../components/schemas/error.yaml#/error"
            examples:
              unexpected_error:
                $ref: "../components/examples/error.yaml#/unexpected_error"
reservations_{reservationId}:
  get:
    summary: Get a specified reservation
    operationId: getReservationsById
    tags:
      - Booking
    parameters:
      - name: reservationId
        in: path
        required: true
        description: The id of the reservation to retrieve
        schema:
          type: integer
          format: int64
    responses:
      '200':
        description: Expected response to a valid request
        content:
          application/json:
            schema:
              $ref: "../components/schemas/booking.yaml#/reservationInfo"
      default:
        description: unexpected error
        content:
          application/json:
            schema:
              $ref: "../components/schemas/error.yaml#/error"
            examples:
              unexpected_error:
                $ref: "../components/examples/error.yaml#/unexpected_error"
  delete:
    summary: Cancel a specified reservation
    operationId: deleteReservationsById
    tags:
      - Booking
    parameters:
      - name: reservationId
        in: path
        required: true
        description: The id of the reservation to cancel
        schema:
          type: integer
          format: int64
    responses:
      '201':
        description: Null response.
      default:
        description: unexpected error
        content:
          application/json:
            schema:
              $ref: "../components/schemas/error.yaml#/error"
            examples:
              unexpected_error:
                $ref: "../components/examples/error.yaml#/unexpected_error"

./schemas/hotel.yaml

hotel:
  type: object
  required:
    - hotelId
    - name
  properties:
    hotelId:
      type: integer
      format: int64
    name:
      type: string
hotels:
  type: array
  maxItems: 100
  items:
    $ref: "#/hotel"

./schemas/booking.yaml

reservation:
  type: object
  required:
    - date
    - guest
    - hotel
  properties:
    date:
      type: string
      format: date
    guest:
      type: string
    hotel:
      $ref: "../schemas/hotel.yaml#/hotel"
reservationInfo:
  type: object
  required:
    - reservationId
    - reservation
  properties:
    reservationId:
      type: integer
      format: int64
    reservation:
      $ref: "../schemas/booking.yaml#/reservation"

./schemas/error.yaml

error:
  type: object
  required:
    - code
    - message
  properties:
    code:
      type: integer
      format: int32
    message:
      type: string

./examples/booking.yaml

reservation:
  summary: Reservation request
  value: |-
    {
      "date": "2023-01-01",
      "guest": "Foo Bar",
      "hotel": {
        "hotelId": 1,
        "name": "HOGE Hotel"
      }
    }

./examples/error.yaml

overload_error:
  summary: Processing overload
  value: |-
    {
      "code": "ERR90001",
      "message": "we couldn't handle your request."
    }
unexpected_error:
  summary: Unexpected error
  value: |-
    {
      "code": "ERR90002",
      "message": "we couldn't handle your request."
    }

まとめ

例示用に作成したファイルが大きくなりすぎたのですが、そのおかけで、ファイル分割によるメンテナンス性向上の効果が実感できました。
実際の現場では、開発時にドキュメントを作成するは大変なのですが、
API仕様をOpenAPIで記載していくことで、Git管理できるし、API利用者に提示することもできるので、それなりにメリットがあると思います。

最後まで読んでくださりありがとうございました。
何かのお役に立てば幸いです。

参考にさせていただいたサイトのURL

Discussion