この記事は ラクスルの2022年アドベントカレンダー 18日目の記事です。
こんにちは!グループ会社のダンボールワンに出向中の 🐈 miyahkun です。現在はフロントエンドエンジニアとして働いています。今回は OpenAPI を利用したクライアントコードの自動生成について紹介したいと思います。
背景
OpenAPI のクライアント生成として OpenAPI Generator というプロジェクトが有名です。弊社でも多くのプロジェクトで使用されており、生成先の言語として TypeScript、HTTP クライアントに axios を用いる typescript-axios というオプションが最も使用されています。
最近 OpenAPI のクライアント生成を新たに行う機会があり、いつものように typescript-axios をインストール・実行しました。そして自動生成されたコードを確認してみると、既存のプロジェクトで使用しているバージョンとは異なる挙動となっていました。
ここでは typescript-axios で気になった挙動の変化と解決策について共有します。
typescript-axios はもうケース変換してくれない
私たちの開発チームでは JSON 形式の API レスポンスにおいて、プロパティを snake_case で提供しています。一方、その API を利用する JavaScript (TypeScript) で書かれたクライアント内では camelCase を使用したくなります。これは JSON API を扱うにあたって非常にありふれた話題かと思います。
せっかくクライアントコードを自動生成するからには、以下のような処理も自動でやってほしいですよね。
- リクエストのパラメーターは camelCase -> snake_case へ変換
- JSON 形式のレスポンスではプロパティを snake_case -> camelCase へ変換
- それに対応する TypeScript の型もケース変換されたものを生成
私たちが既存のプロジェクトで使用しているバージョンの typescript-axios では、これらの要求の一部に応えてくれていました。
- リクエストのパラメーターは生成されたコード内でケース変換
- レスポンスは TypeScript の型としてケース変換したものを生成する。一方で、実体は自前でケース変換する必要がある。
しかし、このケース変換の要求に応えてくれていた生成時のオプション modelPropertyNaming
が v5.3.0 で削除されていることを知りました。削除された理由はこちらの issue で述べられています。
modelPropertyNaming: original
というスキーマ定義を尊重する設定にした場合でも、ドットなどのケース変換不可な文字を含むプロパティ名が意図せず変換されていました。例えば、 product.name
というプロパティ名が product_name
になるといった具合です。
代替案の検討
私のチームは代替案としては以下の3つを挙げました。
modelPropertyNaming
がサポートされている typescript-axios の古いバージョンを使う。- axios-case-converter などでプロパティ名の変換を行い、TypeScript の型を変換する処理は自前で書く。
modelPropertyNaming
がサポートされている Fetch API を利用した typescript-fetch を使用する。
(1) は該当バージョンが 1 年ほど前のリリースということもあり、積極的にメンテナンスされることが期待できないため候補から外しました。
(2) は自動生成されるそれぞれの型に対して、ケース変換を一度通さないといけない点が煩わしいと感じて却下しました。
(3) で述べているように OpenAPI Generator には HTTP クライアントとして Fetch API を使用する方法も用意されています。こちらは typescript-axios で以前にサポートされていたものと同等の modelPropertyNaming
が存在しており、今回はこちらの案を採用することにしました。
私のチームが開発しているサービスでは IE11 のサポートが廃止されたこともあり、Fetch API を気軽に使えるようになりました。これにより Fetch API のポリフィルや axios を使わないことでバンドルサイズを削減できます。また、Node.js v18 から experimental ではありますがフラグなしでグローバルに生えた fetch 関数を利用できるようになったのも採用の後押しとなりました。
参照:New globally available browser-compatible APIs
typescript-fetch と typescript-axios の比較
typescript-fetch と typescript-axios は生成方法のオプションに差異があり、デフォルト値が異なる場合や、対応するオプションがそもそも存在しない場合もあります。私のチームが使うオプションについて、実際に動作させて挙動を確認してみました。
(以下の比較は Docker イメージ openapitools/openapi-generator-cli:v6.2.0
で実行しています。)
オプション | typescript-axios | typescript-fetch |
---|---|---|
API 呼び出し処理の生成先 | apiPackage で指定必須 |
./apis (該当オプションなし) |
モデルの生成先 | modelPackage で指定必須 |
./models (該当オプションなし) |
モデルと API 呼び出し処理のファイル分割 | withSeparateModelsAndApi = false (デフォルト値) |
分割される (該当オプションなし) |
パラメーターの受け渡しにオブジェクトを使用するか | useSingleRequestParameter = false (デフォルト値) |
useSingleRequestParameter = true (デフォルト値) |
これらのオプションを設定することで、私のチームではインターフェースに大きな差異なく使用できています。
typescript-fetch のケース変換処理
typescript-fetch では愚直に snake_case <-> camelCase 間の変換を行なっています。以下がOpenAPI スキーマと自動生成されたケース変換しているコードの一部です。ケースを動的に変換する方式と比較すると生成されるコード量としては多くなってしまいますが、複雑な TypeScript の型変換が不要で個人的には気に入っています。
paths: /search: get: responses: '200': $ref: '#/components/schemas/product' components: schemas: search_response: properties: products: type: array items: $ref: '#/components/schemas/product' type: object product: required: - name - price_tax_included - dot.separated properties: name: type: string price_tax_included: type: integer dot.separated: type: string type: object
// レスポンスのケース変換処理 export function ProductFromJSONTyped(json: any, ignoreDiscriminator: boolean): Product { if ((json === undefined) || (json === null)) { return json; } return { 'name': json['name'], 'priceTaxIncluded': json['price_tax_included'], 'dotSeparated': json['dot.separated'], }; }
最後に
本記事ではケース変換の問題を発端に typescript-fetch を紹介しました。IE 11 サポートの終了や Node.js の対応などで Fetch API を利用できる環境が整ってきました。それに伴い OpenAPI Generator の typescript-fetch は今後より積極的に採用されると思っています。この記事が何かの助けになれば幸いです。