OpenAPI定義ファイルを分割して管理し易くする
はじめに
API仕様書をOpenAPIで定義する際にファイル分割してメンテナンスしやすくしようという話です。
OpenAPI定義はVSCodeで作成していきます。
VSCodeでOpenAPI定義ファイルを作成する際に便利な拡張機能については以下の記事にまとめてあります。
https://zenn.dev/yamatonokuni/articles/bd547c74fe9007
本記事の定義例をVSCodeで表示したプレビューです。
最終的なファイル分割構成
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利用者に提示することもできるので、それなりにメリットがあると思います。
最後まで読んでくださりありがとうございました。
何かのお役に立てば幸いです。
Discussion