📝

OpenAPI定義を用いて型安全にFormDataを扱いたい!

こんにちは、エンジニアの籏野です。

フォルシアの API 開発では OpenAPI 定義を利用し、TypeScript の型定義や各種ソースコードを自動で生成していることがあります。
最近は型の生成にopenapi-typescript、フロントエンドで利用する API クライアントにopenapi-fetchを利用する機会が増えてきました。

openapi-fetch を利用する場合、クエリ/パスパラメータ―や JSON.stringify できるようなボディパラメーターについては特に気にすることなく利用が可能なのですが、ファイルのアップロードを行う場合にどのようにすればよいかがわからなかったので調べました。

公式ドキュメントにも記載されている内容ですが、なかなかたどり着けなかったのでまとめておきます。

OpenAPI 定義の作成

OpenAPI 定義にてアップロードファイルを添付する場合には以下のような定義になります。

requestBody:
  content:
    multipart/form-data:
      schema:
        type: "object"
        properties:
          file:
            type: "string"
            format: "binary"

この定義から、型の生成及びフロントエンドからのリクエストを行っていきます。

型定義の生成

openapi-typescript を使って先の定義を型に変換すると以下のような定義になります。

    requestBody?: {
      content: {
        "multipart/form-data": {
          /**
           * Format: binary
           */
          file: string;
        };
      };
    };

file の型がstringになってしまいました。
実際のリクエストではFileインスタンスを取り扱いたいため、これでは型の不一致が起こってしまいます。

公式ドキュメントによると、openapi-typescript の Node.js API を利用し transform オプションを定義することで、生成される型定義を上書きするのがよいようです。

import openapiTS from "openapi-typescript";

// NOTE specはファイルから読み込んだOpenAPI定義
const typeFileContent = await openapiTS(spec, {
  transform(schemaObject, _metadata): string | undefined {
    if ("format" in schemaObject && schemaObject.format === "binary") {
      return schemaObject.nullable ? "Blob | null" : "Blob";
    }
  },
});

これにより生成される型が Blob になったため、File インスタンスと型の不一致を起こすことなく扱うことができるようになりました。

    requestBody?: {
      content: {
        "multipart/form-data": {
          /**
           * Format: binary
           */
          file: Blob;
        };
      };
    };

フロントエンドからのリクエスト

続いてフロントエンドからのリクエストを行います。
openapi-fetch の細かい利用方法については省略しますが、生成された型の通りにリクエストを行おうとすると以下のようになります。

async function (file: File) {
    return await client.POST("/upload", {
        body: {
            file
        }
    });
}

しかし、上記のコードを実行しても API 側でファイルを取得することができませんでした。
openapi-fetch ではリクエスト時に、ボディパラメーターをシリアライズしているのですが、このシリアライズ方法がデフォルトでは JSON.stringifyになっていました。
ファイル送信時には FormData を利用したいので、シリアライズ方法を設定する必要があります。

async function (file: File) {
    return await client.POST("/upload", {
        body: {
            file
        },
        bodySerializer(body) {
            const formData = new FormData();
            if (body) {
                for (const key in body) {
                    const data = body[key];
                    if (data) {
                        if (data instanceof File) {
                            formData.append(key, data, encodeURI(data.name));
                        } else {
                            formData.append(key, data);
                        }
                    }
                }
            }
            return formData;
        }
    });
}

これにより、API 側でアップロードファイルを受け取ることができるようになりました。

最後に

今回は主に API へのリクエスト部分での OpenAPI 定義活用の Tips を紹介しました。
フォルシアでは他にも様々な部分で OpenAPI 定義を利用した安全な開発を実現していますので、他の知見についてもまた紹介したいと思います。

この記事を書いた人

籏野 拓
2018 年新卒入社

FORCIA Tech Blog

Discussion