😊

FastAPI の OpenAPI定義で oneOf の値を定義する方法

2023/06/05に公開

こんにちは。hayata-yamamoto です。

本記事では、FastAPI を用いて OpenAPI 定義を作成する際に oneOf な値をエクスポートする方法を共有します。「OpenAPIの定義なんて、ちゃんと書かなくてもチーム内で分かればいいやろ」と思う人もいらっしゃるでしょうが、弊社では OpenAPI の定義を用いて TypeScript の型が生成されてしまうため、意図した通りにOpenAPIの定義を記述する必要がありました。

なお、本記事の執筆に際しては FastAPI の以下の Issue を参考にしています。原文で知りたい人はそちらをご確認ください

https://github.com/tiangolo/fastapi/issues/4959

OpenAPI における oneOf, anyOf, allOf の違い

以下のページが詳しいです。
https://swagger.io/docs/specification/data-models/oneof-anyof-allof-not/

どのように書き分けるか

以下のコードを手元で実行してみていただくとわかりやすいです。例として、丸と四角のアイテム情報をPOSTするAPIを用意しました。違いは Annotated を使って、Python の型ヒントにメタデータを付与するかどうかです。Annotated については以下の記事も合わせてご覧ください。

https://docs.python.org/ja/3/library/typing.html#typing.Annotated
https://yiskw713.hatenablog.com/entry/2022/01/25/233000
https://pod.hatenablog.com/entry/2020/07/29/053015

from typing import Annotated, Literal, Union

from fastapi import FastAPI
from pydantic import BaseModel, Field

class Round(BaseModel): 
    kind: Literal['round'] 
    label: str 
    border_radius: float 

class Rectangle(BaseModel): 
    kind: Literal['rectangle'] 
    label: str 
    x: int 
    y: int 
    height: int 
    width: int 

# ここが重要
Item = Annotated[Union[Round, Rectangle], Field(discriminator="kind")]  

class ItemAnyOfRequest(BaseModel): 
   item: Union[Round, Rectangle]

class ItemOneOfRequest(BaseModel): 
   item: Item


app = FastAPI()

@app.post('/item-one-of') 
def post_item(request: ItemOneOfRequest): 
    ...

@app.post('/item-any-of')
def post_any_of_item(request: ItemAnyOfRequest): 
   ...

たったこれだけですが、OpenAPIで出力される結果が変わります。型ヒントのヒントを Annotated で定義するイメージですね。「kind を先に見て、Union[Round, Rectangle] を区別しますよ」と言っています。

トドケールテックブログ

Discussion