こんにちは! API開発の現場で、複雑なリクエストボディやネスト構造のJSONを前に頭を抱えたことはありませんか?
「仕様変更のたびに同じようなスキーマ定義をコピペして修正している……」
「検索条件によって送るデータ構造が変わるポリモーフィズムを、どうOpenAPIで表現すればいいのかわからない……」
もしあなたがそんな悩みを抱えているなら、安心してください。今日を境に、そのストレスとはお別れです。
今回は、Swagger / OpenAPIを使って「複雑なリクエストボディや配列・ネスト構造をエレガントに定義する極意」を、現場の知見をたっぷり詰め込んで優しく解説します。
これをマスターすれば、API設計のスピードが劇的に上がり、フロントエンド開発者との認識のズレも一瞬で消え去りますよ。さあ、一緒に扉を開けましょう!
—
1. なぜSwagger / OpenAPIなのか?(超入門の前提)
まず、これからOpenAPI(旧Swagger)に触れる方に向けて、このツールの本質をひとことで伝えておきます。
Swagger / OpenAPIとは、「人間と機械の両方が読める、APIの共通言語(設計図)」です。
コードを書く前にこの設計図をカチッと作っておくことで、次のような魔法のようなメリットが生まれます。
1. ドキュメントが勝手に最新を維持する(もう古いPDFやWikiに悩まされない)
2. モックサーバーが即座に立ち上がる(フロントエンドがバックエンドの完成を待たずに開発できる)
3. クライアントのSDKコードを自動生成できる(手動のAPIクライアント実装から解放される)
最速の基礎セットアップと「Hello World」
まずは、手元で動かすための最小限の構成を作りましょう。今回は最もポピュラーなYAML形式(OpenAPI 3.0.3)を使います。
プロジェクトルートに `openapi.yaml` というファイルを作成し、以下のコードを貼り付けてみてください。
openapi: 3.0.3
info:
title: 究極のAPI入門
version: 1.0.0
description: 初めてのOpenAPI定義。ここからすべてが始まります。
paths:
/hello:
get:
summary: Hello World API
description: システムが正常に稼働しているかを確かめるためのエンドポイントです。
responses:
‘200’:
description: 成功時のレスポンス
content:
application/json:
schema:
type: object
properties:
message:
type: string
example: “Hello, World!”
これだけで、立派なAPI仕様書の完成です。Swagger UIなどのビジュアライザに読み込ませれば、美しいドキュメントと「Try it out(試行)」ボタンが自動生成されます。
基礎の土台はこれだけ。次からが本番です!
—
2. `$ref` を活用したスキーマのモジュール化と再利用のテクニック
さて、実務でAPIを作ると、何十行もあるユーザー情報や共通エラーレスポンスの定義を何度も何度も書くハメになりがちです。ここで「コピペ職人」になってはいけません。
プロのエンジニアは `$ref`(リファレンス) を使って、部品をパーツ化(モジュール化)します。
実践:コンポーネントとして切り出す
OpenAPIでは、ファイル下部の `components/schemas` にデータ構造の部品を定義し、`$ref` で参照するのが定石です。
openapi: 3.0.3
info:
title: モジュール化されたEC注文API
version: 1.0.0
paths:
/orders:
post:
summary: 注文作成API
requestBody:
required: true
content:
application/json:
schema:
# 注文データのメインスキーマを参照
$ref: ‘#/components/schemas/CreateOrderRequest’
responses:
‘201’:
description: 注文完了
content:
application/json:
schema:
$ref: ‘#/components/schemas/OrderResponse’
==========================================
ここから下が再利用可能なスキーマの部品庫!
==========================================
components:
schemas:
# 住所情報の共通パーツ
Address:
type: object
required:
- postalCode
- prefecture
- city
properties:
postalCode:
type: string
example: “150-0043”
description: 郵便番号(ハイフンなし)
prefecture:
type: string
example: “東京都”
city:
type: string
example: “渋谷区道玄坂”
building:
type: string
example: “ビル名 101号室”
# 注文商品のパーツ(配列の要素として使われる)
OrderItem:
type: object
required:
- productId
- quantity
properties:
productId:
type: string
format: uuid
example: “123e4567-e89b-12d3-a456-426614174000”
quantity:
type: integer
minimum: 1
example: 2
# 注文作成リクエスト本丸
CreateOrderRequest:
type: object
required:
- customerId
- items
- shippingAddress
properties:
customerId:
type: string
example: “cust_998877”
items:
type: array
description: 注文商品の配列(ネスト構造)
items:
$ref: ‘#/components/schemas/OrderItem’ # 配列の型としても$refが使える!
shippingAddress:
$ref: ‘#/components/schemas/Address’ # 先ほど定義した住所を使い回し
# レスポンス用
OrderResponse:
type: object
properties:
orderId:
type: string
example: “ord_112233”
status:
type: string
enum: [pending, paid, shipped, completed]
example: “pending”
ここがプロの知見:
`$ref` を使うことで、仕様変更(例えば「住所にビル名を追加する」など)が発生した際に、`Address` コンポーネントを1箇所修正するだけで、それを利用しているすべてのAPI(会員登録、配送先変更、注文など)の定義が一括で更新されます。保守性が桁違いに向上しますよ。
—
3. 一覧取得APIにおけるページネーションとフィルタリングパラメータの書き方
次に、実務で最も実装頻度が高い「一覧取得(GET)API」のクエリパラメータの書き方です。
ページネーション(オフセット型やカーソル型)や、多様なフィルタリング条件をどう美しく表現するかを見てみましょう。
paths:
/products:
get:
summary: 商品一覧取得(高度な検索・ページネーション付き)
parameters:
# — ページネーション制御 —
- name: limit
in: query
description: 1ページあたりに取得する最大件数
required: false
schema:
type: integer
default: 20
minimum: 1
maximum: 100
- name: offset
in: query
description: 取得開始位置(オフセット)
required: false
schema:
type: integer
default: 0
minimum: 0
# — フィルタリング条件 —
- name: category
in: query
description: カテゴリによる絞り込み(複数指定可にする場合はexplodeなどを活用)
required: false
schema:
type: string
enum: [electronics, books, clothing, food]
- name: minPrice
in: query
description: 最低価格(これ以上の価格を抽出)
required: false
schema:
type: integer
minimum: 0
example: 1000
- name: searchKeyword
in: query
description: 商品名や説明文の部分一致検索キーワード
required: false
schema:
type: string
example: “ワイヤレスイヤホン”
# — ソート順 —
- name: sortBy
in: query
description: ソート基準のフィールド
required: false
schema:
type: string
enum: [price, createdAt, popularity]
default: createdAt
- name: sortOrder
in: query
description: 昇順か降順か
required: false
schema:
type: string
enum: [asc, desc]
default: desc
responses:
‘200’:
description: 検索結果一覧とメタ情報の返却
content:
application/json:
schema:
type: object
properties:
totalCount:
type: integer
description: 該当する全件数
example: 154
items:
type: array
items:
$ref: ‘#/components/schemas/Product’ # 商品スキーマ
pagination:
type: object
properties:
limit:
type: integer
example: 20
offset:
type: integer
example: 0
hasNext:
type: boolean
example: true
ここがプロの知見:
クエリパラメータは `in: query` で1つずつ定義するのが基本ですが、共通のページネーションパラメータ(`limit` や `offset`)があちこちのエンドポイントに出現する場合は、これも `components/parameters` として共通化できます。
また、`enum` を使って「指定できる値の選択肢」を明確に制約しておくことで、クライアント側が不正なパラメータを送信するミスを未然に防ぐことができます。
—
4. `oneOf`, `anyOf`, `allOf` を使ったポリモーフィズムの表現方法
さあ、ここが今回の山場です。
「決済手段によって、クレジットカード情報が必要だったり、銀行振込情報が必要だったりする。リクエストボディの構造が動的に変わる(ポリモーフィズム)んだけど、どう書けばいいの?」
そんな難問を鮮やかに解決するのが、`oneOf`、`anyOf`、`allOf` という3つのキーワードです。それぞれの使い分けをマスターしましょう。
- `oneOf`: いずれか「1つだけ」に完全に一致する必要がある(排他的)
- `anyOf`: いずれかのスキーマに「1つ以上」一致していればよい
- `allOf`: すべてのスキーマの「合成(継承のようなもの)」を行う
実践:支払い方法によってボディが変わる決済API
「クレジットカード決済」と「コンビニ決済」で、リクエストボディのデータ構造がまったく異なるケースを `oneOf` で表現してみましょう。
paths:
/payments:
post:
summary: 決済実行API
description: 支払い方法(paymentMethod)に応じて、必要なペイロード構造が切り替わります。
requestBody:
required: true
content:
application/json:
schema:
# どちらか一つのスキーマに厳密に一致する必要がある
oneOf:
- $ref: ‘#/components/schemas/CreditCardPayment’
- $ref: ‘#/components/schemas/ConvenienceStorePayment’
# どのスキーマに合致するかを識別するためのプロパティ名を指定( discriminator )
discriminator:
propertyName: paymentMethod
mapping:
creditCard: ‘#/components/schemas/CreditCardPayment’
cvs: ‘#/components/schemas/ConvenienceStorePayment’
responses:
‘200’:
description: 決済成功
components:
schemas:
# 1つ目のパターン:クレジットカード決済
CreditCardPayment:
type: object
required:
- paymentMethod
- amount
- token
properties:
paymentMethod:
type: string
description: 識別子(常に “creditCard”)
example: “creditCard”
amount:
type: integer
example: 5000
token:
type: string
description: フロントエンドで発行された決済トークン
example: “tok_12345abcdef”
# 2つ目のパターン:コンビニ決済
ConvenienceStorePayment:
type: object
required:
- paymentMethod
- amount
- customerEmail
- convenienceStoreType
properties:
paymentMethod:
type: string
description: 識別子(常に “cvs”)
example: “cvs”
amount:
type: integer
example: 5000
customerEmail:
type: object
# ネストしたメールアドレス情報など…
properties:
email:
type: string
format: email
example: “user@example.com”
convenienceStoreType:
type: string
enum: [seven_eleven, lawson, familymart,Ministop]
example: “lawson”
ここがプロの知見:
ポリモーフィズムを表現する際、`discriminator`(識別子)を組み合わせるのがベストプラクティスです。これがあると、コードジェネレータ(OpenAPI Generatorなど)が自動的に「どの型にキャストすべきか」を判断するスマートなコードを生成してくれます。バックエンドのポリモーフィックなクラス設計(継承やインターフェース)とも美しくシンクロします。
—
5. おわりに:毎日の開発が劇的に楽になる未来へ
お疲れ様でした!
今回は、Swagger / OpenAPIを用いた以下の実戦的なテクニックを解説しました。
1. `$ref` によるモジュール化で、重複を排除しメンテナンス性を高める方法
2. 堅牢な一覧取得APIにおけるページネーション・フィルタリング・ソートの設計
3. `oneOf` と `discriminator` を使った高度なポリモーフィズム(条件分岐するリクエスト)の表現
最初は少し難しく感じるかもしれませんが、一度この「型にはまった美しい設計」を味わうと、もうアドホックなJSON設計には戻れなくなります。
これをマスターすれば、フロントエンドチームからの「このAPI、どういうデータ送ればいいんですか?」という質問は激減し、あなた自身の開発スピードも劇的に楽になりますよ。
今日の学びを、ぜひあなたの次のプロジェクトのAPI設計に活かしてみてください。それでは、快適なAPIライフを!