OpenAPI Generatorに適したOpenAPIの書き方

ogp

はじめに

こんにちは! 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つ設定する

paths:
  /pet/findByStatus:
    get:
      tags:
        - pet
      operationId: findPetsByStatus

理由

tagsoperationIdは自動生成されたコードではそれぞれクラス名、メソッド名になります。

それぞれ設定しないと自動で名前が付与されてしまいます。意図しない名前が付与されてしまうことを防ぐため、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では、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。

corp.zozo.com


  1. ただしこの挙動は自動生成する言語によって変わる可能性があります。WEARではGoでパースエラーになったことを確認しています。
カテゴリー