本業ではないですが、部署内で小さなアプリを開発したりしています。
今回、あまり知らなかった仕様と避けるべきコードの仕様を組み合わせてしまったことで、エラーが出てしまったので書き記します。
何が起こったのか
FastAPIで入力されたデータから結果を返すREST APIを作成しています。
ざっくりと書けば、下のようにとてもシンプルなプログラムです(本当はもう少し複雑ですが、議論の単純化のためにシンプルに書いています)。
from fastapi import FastAPI
from pydantic import BaseModel
from typing import Optional
class Answer(BaseModel):
age: int
q1: int
q2: int
q3: Optional[int]
app = FastAPI()
@app.post("/calc/")
async def calculation(answer: Answer):
answer_dict = answer_score.dict()
return calc.main(answer_dict)
フロントエンド側では、年齢により回答する必要のない項目はそもそも入力しないという設定になっています。
そのため、上の例で言えば下のようにq3の回答がないjsonが入力される可能性があります。
{"q1": 2, "q2":1}
それで稼働していたら、同僚より計算ができないという連絡があり確認。
元々の計算ファイル(answer.py)には問題がなかったので、なにかおかしな挙動をしていないか確認してみることにしました。
fastAPIでありがたいのはAPIドキュメンテーションが自動生成されること。早速http://localhost:8000/docs/ を確認してみます。
誤った値がanswer.pyに渡っていないかを確認するため、
@app.post("/calc/")
async def calculation(answer: Answer):
return answer
としてみました。すると返ってきたjsonは
{"q1": 2, "q2": 1, "q3": null}
となっていました。
answer.pyにはageによって条件分岐を記載しており、その場合はq3が入力されていない前提で処理を書き進めてしまっており、そのためにエラーが起きたようでした。
解決策
そもそも入力される値を信頼しすぎたコードを書くべきではないのかもしれません。q3が入力されていたら削除したり、他の入力があっても問題のないようなコードを書くべきでした。fastAPIとフロントエンド側でValidationされているから大丈夫だろう、と思ったがために起こった罠でした。
今回は時間がないので力技でなんとかしました。処理速度は落ちそうだけど気にしないことにしました。
from fastapi import FastAPI
from pydantic import BaseModel
from typing import Optional
class Answer(BaseModel):
age: int
q1: int
q2: int
q3: Optional[int]
app = FastAPI()
@app.post("/calc/")
async def calculation(answer: Answer):
answer_dict = answer_score.dict()
answer_dict_ = {k: v for k, v in answer_dict.items() if v is not None}
return calc.main(answer_dict_)
このようにNoneの値を削除した辞書を用意することで問題なく動くようになりました。
学んだこと
FastAPIのValidation SchemaでのOptionalは、値が入力されていないときはnullが返ってくるので注意
そもそもdictの中身にnull(None)が入ってくるだけでエラーが起こるようなコードを書いてはいけない。