はじめに
こんにちは! WEARバックエンドブロックの高久です。
WEARではOpenAPI(Swagger)を使って、アプリやWebのクライアントが利用するAPIを定義しています。そして先日、開発効率化のためにOpenAPI GeneratorでOpenAPIからAPIクライアントコードを自動生成、活用できるように整備をしました。その中でOpenAPI Generatorに適したOpenAPIの書き方のポイントがいくつかあったので、内容を紹介していきます。
想定読者
- OpenAPIを現在利用している、またはこれから利用する予定の方
- OpenAPI Generatorを利用したコード自動生成を検討している方
背景
当初WEARではAPIクライアントコードはOpenAPIでのAPI定義を基に各クライアントが手動で実装していました。しかし手動で実装すると初期の実装コストや変更時の追従コストがかかるため、開発効率化のためにOpenAPI Generatorを利用してAPIクライアントコードを自動生成することにしました。
ただ、そのままのOpenAPIでコードを自動生成しようとすると、エラーが発生したり、自動で名付けられたクラス名が不相応だったりと実用できるコードではありませんでした。そのためTry&Errorで実用可能なコードになるまで改善を繰り返しました。今回はその改善から得たOpenAPI GeneratorフレンドリーなOpenAPIの書き方を紹介します。
前提
- OpenAPIのバージョンは3.0.0です。
- 言語によってOpenAPI Generatorの挙動が変わるため、本記事に記載する事象が全ての言語に当てはまる訳ではありません。WEARではクライアントの言語としてSwift, Kotlin, Go, TypeScriptを利用しているため、今回はそのいずれかの言語で発生した内容となります。
書き方のポイント
- tags、operationIdを1エンドポイントにつき1つ設定する
- レスポンススキーマでenumを使わない
- anyOf、oneOfを使わない
- type:object単位で/components/schemas配下にスキーマ化する
- enumに各言語の予約語を使用しない
tags、operationIdを1エンドポイントにつき1つ設定する
paths:
/pet/findByStatus:
get:
tags:
- pet
operationId: findPetsByStatus
理由
tags、operationIdは自動生成されたコードではそれぞれクラス名、メソッド名になります。
それぞれ設定しないと自動で名前が付与されてしまいます。意図しない名前が付与されてしまうことを防ぐため、1エンドポイントにつき1つ設定して適切なクラス名、メソッド名を付与するようにしています。
(以降Rubyでの生成結果)
以下、tags、operationIdを設定した例。
class PetApi
・・・
def find_pets_by_status(opts = {})
・・・
end
end
以下、tags、operationIdを設定しなかった例。このように自動で名付けられます。
class DefaultApi
・・・
def pet_find_by_status_get(opts = {})
・・・
end
end
またtagsはOpenAPIでは配列形式で設定が可能ですが、タグを2つ以上設定すると設定したタグのクラスに同じメソッドが重複して定義されてしまいます。そのため1エンドポイントにつきタグは1つだけ付与することを推奨します。
レスポンススキーマでenumを使わない
いい例。
components:
schemas:
status:
type: string
悪い例。
components:
schemas:
status:
type: string
enum:
- placed
- approved
- delivered
理由
enumに値を追加したい場合に考慮することが増えるためです。
APIレスポンスのenumに値を追加したい場合、クライアントコード側でもenumが追加された状態でないと、自動生成コードでパースエラーになることがわかりました1。
APIとクライアントでenumを追加するタイミングを合わせる必要があるのですが、そのためにはアプリの強制アップデート等の対応が必要になってきます。その考慮が必要など対応工数が大きくかかるため、WEARではレスポンススキーマからはenumを削除し、どの値でも受け付けられるようにしました。
なお、リクエストパラメータのenumに関しては上記の課題は影響しないため、使用しても特に問題ありません。
anyOf、oneOfを使わない
理由
anyOf、oneOfはまだ完全にサポートされておらず、動作が不安定なためです。2023年3月現在、こちらのissueでまだ議論が行われております。
幸いWEARではanyOf, oneOfの使用箇所が少なかったため、使用箇所は排除し今後使わない方針としました。
type:object単位で/components/schemas配下にスキーマを作成し、ref参照する
いい例。
components:
schemas:
Pet:
type: object
properties:
id:
type: integer
category:
$ref: '#/components/schemas/Category'
Category:
type: object
properties:
id:
type: integer
name:
type: string
悪い例(Petオブジェクトのなかに、categoryオブジェクトを定義している)
components:
schemas:
Pet:
type: object
properties:
id:
type: integer
category:
type: object
properties:
id:
type: integer
name:
type: string
理由
type:objectの中に更にobjectを定義すると、そのobjectのモデル名が自動で付与されてしまうためです。
以下は「悪い例」の定義で自動生成した際のソースコードです。
module OpenapiClient
class PetCategory
・・・
end
end
自動的にPetCategoryというモデル名が名付けされています。言語によってはInlineObject{連番}というような名付けがされることもあり、可読性、保守性の面で実用的ではありません。type:objectの中にobjectが必要になったら、/components/schemas配下にスキーマを1つ作りref参照することで、自動で名付けされることを防げます。
また以下のようなレスポンスボディの定義も同様です。
続きはこちら