FastAPI の OpenAPI スキーマから openapi-generator でファイルとネストされたリクエストボディを受け取るエンドポイントの型が定義されない問題への対処法
- # FastAPI
2021年01月03日
前提
FastAPI では、開発サーバーの /openapi.json
から OpenAPI スキーマが提供されます
このスキーマから、openapi-generator で TypeScript コードを生成します
私の場合は、typescript-axios ジェネレータ を使っています
環境
$ pip freeze | grep fastapi
fastapi==0.62.0
$ yarn list | grep openapi
├─ @openapitools/[email protected]
問題
multipart/form-data
でファイルとネストされたリクエストボディを同時に受け取るエンドポイントにて、適切に型定義が生成されません
例として、以下の endpoint1
server.py
from fastapi import FastAPI, UploadFile, File
from fastapi.openapi.utils import get_openapi
from pydantic import BaseModel
app = FastAPI()
class Arg1(BaseModel):
hoge: string
@app.post("/endpoint1/")
async def endpoint1(body: Arg1, file: UploadFile = File(...)) -> Dict[str, Any]:
# ...
class Arg2(BaseModel):
huga: string
@app.post("/endpoint2/")
async def endpoint2(body: Arg2) -> Dict[str, Any]:
# ...
スキーマとしては、以下(多少簡略化してます)
schema.json
{
"openapi": "3.0.3",
"info": {
"title": "Sample API",
"version": "0.0.0"
},
"paths": {
"/endpoint1/": {
"post": {
"requestBody": {
"content": {
"multipart/form-data": {
"schema": {
"$ref": "#/components/schemas/schema1"
}
}
}
},
"responses": {
"201": {
"description": "sample endpoint",
"content": {
"application/json": {
"schema": {}
}
}
}
}
}
},
"/endpoint2/": {
"post": {
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/arg2"
}
}
}
},
"responses": {
"201": {
"description": "sample endpoint 2",
"content": {
"application/json": {
"schema": {}
}
}
}
}
}
}
},
"components": {
"schemas": {
"schema1": {
"title": "Schema1",
"required": [
"arg",
"file"
],
"type": "object",
"properties": {
"arg": {
"$ref": "#/components/schemas/arg1"
},
"file": {
"title": "File",
"type": "string",
"format": "binary"
}
}
},
"arg1": {
"title": "Arg1",
"type": "object",
"properties": {
"hoge": {
"type": "string"
}
}
},
"arg2": {
"title": "Arg2",
"type": "object",
"properties": {
"huga": {
"type": "string"
}
}
}
}
}
}
このスキーマから、以下のコマンドで型定義を生成します
$ openapi-generator-cli generate -g typescript-axios -i http://127.0.0.1:5000/schema.json -o ./openapi
で、以下のように生成されます (Arg2
は生成されていますが、Arg1
が生成されていないのがわかります)
openapi/api.ts
// いろいろ省略
/**
*
* @export
* @interface Arg2
*/
export interface Arg2 {
/**
*
* @type {string}
* @memberof Arg2
*/
huga?: string;
}
/**
* DefaultApi - object-oriented interface
* @export
* @class DefaultApi
* @extends {BaseAPI}
*/
export class DefaultApi extends BaseAPI {
/**
*
* @param {Arg2} [arg2]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof DefaultApi
*/
public endpoint2Post(arg2?: Arg2, options?: any) {
return DefaultApiFp(this.configuration).endpoint2Post(arg2, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {Arg1} arg
* @param {any} file
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof DefaultApi
*/
public endpoint1Post(arg: Arg1, file: any, options?: any) {
return DefaultApiFp(this.configuration).endpoint1Post(arg, file, options).then((request) => request(this.axios, this.basePath));
}
}
typescript-fetch
とか、多言語のジェネレータもいくつか試してみましたが、同じ問題がありました
解決策
とりあえず公式のイシューを覗いてみて
この辺りが同じ問題っぽかったですけど、特に解決に取り組まれてる様子もないので、現時点ではこちらで対応する必要がありそうでした
解決策としては、
multipart/form-data
がダメなだけなので、別のエンドポイントを用意してそちらの引数に指定することで生成する--dry-run
で実行すると、一応差分が残るみたいなのでArg1
を自前で書く- グローバル型宣言に
Arg1
を自前で書く
辺りが考えれらます
後者 2 つは FastAPI
側での変更に伴って型宣言を手動で書き換えなくてはならないので、1 を採用します
server.py
from pydantic import BaseModel
class AdditinalSchema(BaseModel):
arg1: Arg1
@app.post("/additional_schema/")
async def additional_schema(body: AdditinalSchema) -> None:
raise RuntimeError('Do no call this endpoint')
こんな感じで適当なエンドポイントを用意して、AdditionalSchema
のプロパティに追加していけば型定義が生成されてそのまま使うことができます