はじめに
こんにちは! 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参照することで、自動で名付けされることを防げます。
また以下のようなレスポンスボディの定義も同様です。
悪い例。
paths: /pet/{petId}: get: parameters: - name: petId in: path schema: type: integer responses: '200': content: application/json: schema: type: object properties: id: type: integer name: type: string category: $ref: '#/components/schemas/Category'
いい例。
paths: /pet/{petId}: get: parameters: - name: petId in: path schema: type: integer responses: '200': content: application/json: schema: $ref: '#/components/schemas/GetPetResponse'
enumに各言語の予約語を使用しない
理由
自動生成したコードでシンタックスエラーになるためです。
WEARで発生した例を紹介します。あるパラメータでenumにopen
を使っていました。しかしKotlinではopen
が修飾子に当たるため、自動生成したコードでシンタックスエラーになることがありました。各言語で予約語が異なるので、全てを考慮して命名することは難しいのですが、意識しておく必要があります。
まとめ
OpenAPI GeneratorフレンドリーなOpenAPIの書き方をいくつかご紹介しました。
WEARでは修正箇所のボリュームが多かったため、問題度合いやエンドポイントから優先度を立てて修正していきました。修正コストはかかったものの、それを上回るメリットがありました。
OpenAPIを使っていてOpenAPI Generatorでのコード自動生成を検討している方がいれば、是非参考にしてみてください。
最後に
WEARでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。
- ただしこの挙動は自動生成する言語によって変わる可能性があります。WEARではGoでパースエラーになったことを確認しています。↩