こんにちは。インサイトテクノロジーの吉﨑です。
そこにはAPI実装と自然言語で書かれたAPIマニュアルがありました。
これを再利用しやすくするため機械で読めるOpenAPIのspecファイルを作りました。
このときに遭遇したschema定義とAPIのリクエストに使うパラメーターが同一でない場合に見通しをよくする方法について書きます。
OpenAPIそのものや関連ツールの説明は最小限にしているため、OpenAPIのサンプルコードが読める方や定義を書いてみたことがある方が対象です。
OpenAPI
https://swagger.io/specification/
OpenAPIはRESTful API記述の仕様の1つです。
フォーマットにしたがってJSONまたはYAMLでspecファイルを記述します。
specファイルから周辺ツールによってドキュメントページや(モック)サーバーコード、クライアントコードの生成が可能です。
エディタ
専用エディタまたはその他エディタ向けのプラグインはいくつかあります。
https://swagger.io/tools/swagger-editor/
の”Live Demo”でオンラインのエディタを試すことができます。
https://github.com/swagger-api/swagger-editor#running-the-image-from-dockerhub
Docker版も使用できます。
Swagger Editorは左にエディタ、右にリアルタイムプレビューの2ペイン構成です。
初期サンプルのフォーマットバージョンはSwagger2.0となっているので新規に書く場合はOpenAPI3.0仕様に変更をおすすめします。
変更前
変更後
仕様
記事データを操作する架空のAPIを例にします。
機能
- このAPIは記事の作成・取得(全件・一件)・更新・削除ができます。
- 記事作成時に承認者のuser_idを指定します。
- 記事作成時にSNSへの通知の有無を指定します。
モデル
APIの扱うモデルを定義します。
APIクライアントとAPIサーバーの間の関心に基づいたモデルなので必ずしもデータベースなどへの格納形式とは対応しません。
このモデルには作成時のみに指定するプロパティ、作成時に指定するが変更不可のプロパティ、作成時に自動で作られるプロパティがあります。
型 | 名前 | 作成時指定 | 更新可能 | 返却 | 備考 |
integer | id | × | × | 〇 | 記事ID |
integer | user_id | × | × | 〇 | 投稿者のユーザーID |
string | title | 〇 | 〇 | 〇 | |
string | content | 〇 | 〇 | 〇 | |
integer | reviewer_user_id | 〇 | × | 〇 | 投稿時承認者のユーザーID |
boolean | notify_by_sns | 〇 | × | × | 投稿時にSNS通知する |
エンドポイント
APIのエンドポイントを定義します。
要求プロパティはモデルの表に対応します。
メソッド | パス | 説明 | 要求プロパティ | 備考 |
POST | /articles | 作成 | 作成時指定 = 〇 | |
GET | /articles | 全件取得 | 無し | 返却は配列 |
GET | /articles/{id} | 1件取得 | 無し | |
PATCH | /articles/{id} | 更新 | 更新可能 = 〇 | |
DELETE | /articles/{id} | 自動生成 | 無し |
OpenAPI記述
定義したモデルとAPI仕様をOpenAPIで記述していきます。
info, host, schema, securityなどの項目は省略しています。
コードは以下のバージョン指定で確認しています。
- OpenAPI 3.0.1
- Swagger Editor v3.15.10
OpenAPIバージョンはspecファイルの先頭で、Swagger EditorバージョンはDocker版のタグで指定しました。
基本的にはオンラインのSwagger Editorに貼り付けて動作するはずです。
STEP1. 取得可能属性でschemaを定義してリクエストは個別に定義する
まずは最も使う頻度が多い返却データの形式でschemaを定義してレスポンスで利用します。
リクエストごとに異なる形式は個別に記述します。
openapi: 3.0.1
info:
title: "STEP1"
description: ""
version: ""
paths:
/articles:
post:
tags:
- articles
summary: 記事作成
operationId: createArticle
requestBody:
description: 記事作成データ
required: true
content:
application/json:
schema:
type: object
properties:
title:
type: string
description: タイトル
example: CRUDでプロパティが変わるモデルをOpenAPIで書くときの定義分割
content:
type: string
description: 内容
example: |
ヘッダー
内容1
内容2
review_user_id:
type: integer
format: int64
description: 投稿時承認者のユーザーID
example: 2
notify_by_sns:
type: boolean
description: 投稿時にSNS通知する
example: true
responses:
default:
description: 作成済み記事
content:
application/json:
schema:
$ref: '#/components/schemas/Article'
get:
tags:
- articles
summary: 記事一覧取得
operationId: getArticleList
responses:
default:
description: 記事一覧
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Article'
/articles/{id}:
get:
tags:
- articles
summary: 記事取得
operationId: getArticle
parameters:
- $ref: '#/components/parameters/ArticleId'
responses:
default:
description: 記事
content:
application/json:
schema:
$ref: '#/components/schemas/Article'
patch:
tags:
- articles
summary: 記事更新
operationId: updateArticle
parameters:
- $ref: '#/components/parameters/ArticleId'
requestBody:
description: 記事更新データ
required: true
content:
application/json:
schema:
type: object
properties:
title:
type: string
description: タイトル
example: CRUDでプロパティが変わるモデルをOpenAPIで書くときの定義分割
content:
type: string
description: 内容
example: |
ヘッダー
内容1
内容2
responses:
default:
description: 更新済み記事
content:
application/json:
schema:
$ref: '#/components/schemas/Article'
delete:
tags:
- articles
summary: 記事削除
operationId: deleteArticle
parameters:
- $ref: '#/components/parameters/ArticleId'
responses:
default:
description: 削除済み記事
content:
application/json:
schema:
$ref: '#/components/schemas/Article'
components:
parameters:
ArticleId:
name: id
in: path
description: 記事ID
required: true
schema:
type: integer
format: int64
schemas:
Article:
type: object
description: 記事
properties:
id:
type: integer
format: int64
description: 記事ID
example: 1
user_id:
type: integer
format: int64
description: 投稿者のユーザーID
example: 4
title:
type: string
description: タイトル
example: CRUDでプロパティが変わるモデルをOpenAPIで書くときの定義分割
content:
type: string
description: 内容
example: |
ヘッダー
内容1
内容2
review_user_id:
type: integer
format: int64
description: 投稿時承認者のユーザーID
example: 2
長いですね。
コード化すると文章で書かれているものより構造が見えやすくなり共通箇所が目立つので使いまわせるようにしたくなります。
STEP2. 取得可能属性のschemaにreadOnly, writeOnly属性を追加する
次はモデルの属性をすべてschemaにまとめてreadOnlyとwriteOnly属性を使います。
- readOnly
- 取得のみで使用する。
- Request Bodyに現れなくなり、Responseにのみ現れるようになります。
- writeOnly
- 送信でのみ使用する。
- Responseに現れなくなり、Request Bodyにのみ現れるようになります。
openapi: 3.0.1
info:
title: "STEP2"
description: ""
version: ""
paths:
/articles:
post:
tags:
- articles
summary: 記事作成
operationId: createArticle
requestBody:
description: 記事作成データ
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/Article'
responses:
default:
description: 作成済み記事
content:
application/json:
schema:
$ref: '#/components/schemas/Article'
get:
tags:
- articles
summary: 記事一覧取得
operationId: getArticleList
responses:
default:
description: 記事一覧
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Article'
/articles/{id}:
get:
tags:
- articles
summary: 記事取得
operationId: getArticle
parameters:
- $ref: '#/components/parameters/ArticleId'
responses:
default:
description: 記事
content:
application/json:
schema:
$ref: '#/components/schemas/Article'
patch:
tags:
- articles
summary: 記事更新
operationId: updateArticle
parameters:
- $ref: '#/components/parameters/ArticleId'
requestBody:
description: 記事更新データ
required: true
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/Article'
- properties: # !!! アドホックな属性上書き
review_user_id:
readOnly: true
notify_by_sns:
writeOnly: false # writeOnly, readOnlyがともにtrueは未定義
readOnly: true
responses:
default:
description: 更新済み記事
content:
application/json:
schema:
$ref: '#/components/schemas/Article'
delete:
tags:
- articles
summary: 記事削除
operationId: deleteArticle
parameters:
- $ref: '#/components/parameters/ArticleId'
responses:
default:
description: 削除済み記事
content:
application/json:
schema:
$ref: '#/components/schemas/Article'
components:
parameters:
ArticleId:
name: id
in: path
description: 記事ID
required: true
schema:
type: integer
format: int64
schemas:
Article:
type: object
description: 記事
properties:
id:
type: integer
format: int64
description: 記事ID
example: 1
readOnly: true
user_id:
type: integer
format: int64
description: 投稿者のユーザーID
example: 4
readOnly: true
title:
type: string
description: タイトル
example: CRUDでプロパティが変わるモデルをOpenAPIで書くときの定義分割
content:
type: string
description: 内容
example: |
ヘッダー
内容1
内容2
review_user_id:
type: integer
format: int64
description: 投稿時承認者のユーザーID
example: 2
notify_by_sns:
type: boolean
description: 投稿時にSNS通知する
example: true
writeOnly: true
RequestとResponseでschemaを使いまわすことができました。
ただし、patchリクエストに場当たり的な対応が残っています。
schema:
allOf:
- $ref: '#/components/schemas/Article'
- properties: # !!! アドホックな属性上書き
review_user_id:
readOnly: true
notify_by_sns:
writeOnly: false # writeOnly, readOnlyがともにtrueは未定義
readOnly: true
schemaの定義でのwriteOnly属性では、リクエストによって使用するしないが異なるプロパティの対応ができないので隠したい属性の箇所をreadOnly属性で上書きしています。
STEP3. プロパティの属性ごとにモデルを分けて結合する
大きな1つのモデルに対してreadOnly/writeOnlyの指定だけで取り回すと扱いにくい場所があることがわかりました。
そこで、プロパティの扱いごとにモデルを分けて必要に応じてallOfで結合します。
openapi: 3.0.1
info:
title: "STEP3"
description: ""
version: ""
paths:
/articles:
post:
tags:
- articles
summary: 記事作成
operationId: createArticle
requestBody:
description: 記事作成データ
required: true
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ArticleDynamicProps'
- $ref: '#/components/schemas/ArticleStaticProps'
- $ref: '#/components/schemas/ArticlePostOption'
responses:
default:
description: 作成済み記事
content:
application/json:
schema:
$ref: '#/components/schemas/Article'
get:
tags:
- articles
summary: 記事一覧取得
operationId: getArticleList
responses:
default:
description: 記事一覧
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Article'
/articles/{id}:
get:
tags:
- articles
summary: 記事取得
operationId: getArticle
parameters:
- $ref: '#/components/parameters/ArticleId'
responses:
default:
description: 記事
content:
application/json:
schema:
$ref: '#/components/schemas/Article'
patch:
tags:
- articles
summary: 記事更新
operationId: updateArticle
parameters:
- $ref: '#/components/parameters/ArticleId'
requestBody:
description: 記事更新データ
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ArticleDynamicProps'
responses:
default:
description: 更新済み記事
content:
application/json:
schema:
$ref: '#/components/schemas/Article'
delete:
tags:
- articles
summary: 記事削除
operationId: deleteArticle
parameters:
- $ref: '#/components/parameters/ArticleId'
responses:
default:
description: 削除済み記事
content:
application/json:
schema:
$ref: '#/components/schemas/Article'
components:
parameters:
ArticleId:
name: id
in: path
description: 記事ID
required: true
schema:
type: integer
format: int64
schemas:
ArticleDynamicProps:
type: object
properties:
title:
type: string
description: タイトル
example: CRUDでプロパティが変わるモデルをOpenAPIで書くときの定義分割
content:
type: string
description: 内容
example: |
ヘッダー
内容1
内容2
ArticleStaticProps:
type: object
properties:
review_user_id:
type: integer
format: int64
description: 投稿時承認者のユーザーID
example: 2
Article:
type: object
description: 記事
allOf:
- properties:
id:
type: integer
format: int64
description: 記事ID
example: 1
user_id:
type: integer
format: int64
description: 投稿者のユーザーID
example: 4
- $ref: "#/components/schemas/ArticleDynamicProps"
- $ref: "#/components/schemas/ArticleStaticProps"
ArticlePostOption:
type: object
properties:
notify_by_sns:
type: boolean
description: 投稿時にSNS通知する
example: true
作成時指定可能で変更可能なプロパティのArticleDynamicPropsモデル、作成時指定で変更不可能なプロパティのArticleStaticPropsモデルを定義します。
そして、それらをallOfで結合して自動生成されるプロパティを加えてArticleモデルとします。
このように分割することでリクエストごとに必要なモデルを選択して利用しやすくなります。
ここでは作成時のみに使用するプロパティもArticlePostOptionモデルとして定義しました。
場合によっては自動生成されるプロパティも別モデルにしたりもできます。
終わりに
APIごとにモデルに要求するプロパティが異なる場合に便利になるモデル分割の例について、ストレートに書く場合、readOnly/writeOnly属性を使う場合と並べて紹介しました。
当然のことながらモデルの分割が適しているかはAPI仕様によって変わってきます。このような方法もあると思い出してもらえると幸いです。
また、こういったフォーマットに落とし込むのが難しいと感じたときはもしかしたらAPI仕様そのものに手を入れるときかもしれません。機械で読めるようにルール化された仕様で書くことは人間が仕様を整理して理解するためにも役立ちます。
その他
今回は紹介できませんでしたがドキュメントページ生成にはReDocがおすすめです。
https://github.com/Redocly/redoc