
STEP 3では、POST APIを作成し、JSON形式のリクエストボディを受け取る方法を学びます。
Pydanticを使ってリクエストデータの型や必須項目を定義し、FastAPIがどのように入力値を検証するのか確認します。
さらに、Fieldを使った値の制約も追加し、APIに不正なデータが送られた場合の動作について理解します。

それではSTEP3を始めましょう!
今回学ぶこと
今回のステップで学ぶ内容を一覧で出してもらいました。

STEP3では以下の内容を学習します。
- POST API・リクエストボディ・Pydanticの役割を理解する
- Pydanticモデルを定義する
- POST APIを作成してJSONを受け取る
- Swagger UIからPOSTリクエストを送信して確認する
- 必須項目・型チェックなどPydanticのバリデーションを確認する
- Fieldを使った入力値の制約を追加する
- 自力課題
学習開始
それでは学習をはじめます。
POST API・リクエストボディ・Pydanticの役割を理解する

たとえばクライアントから次のJSONを送ります。このJSONが リクエストボディ(Request Body) です。
{
"name": "Keyboard",
"price": 5000,
"description": "Mechanical keyboard"
}
FastAPI公式ドキュメントでも、クライアントからAPIへデータを送る場合はリクエストボディを使い、その宣言にはPydanticモデルを利用する方法が基本として説明されています。

ここで登場するのがPydanticです。
Pydanticはリクエストボディの構造を定義し、入力値を検証するものです。
たとえば、以下のように「受け取るJSONはどういう構造なのか」をPythonの型を使って定義します。
name → str
price → int
description → str
FastAPIはその定義を見て、概念的には次のような処理を行います。
JSONリクエスト
↓
FastAPIが読み取る
↓
Pydanticモデルに照らして検証
↓
型変換できるものは変換
↓
問題なし → 関数を実行
問題あり → バリデーションエラー
さらに、このモデル情報からJSON SchemaやOpenAPIの情報も生成されるため、Swagger UIにもリクエスト形式が自動的に反映されます。
DRFのSerializerに近い役割と考えるとイメージしやすいかもしれません。
ただし完全に同じものではありません。
DRFのSerializerは、入力値の検証だけでなく、モデルとの連携やcreate() / update()などDRF独自の責務もかなり持っています。
一方Pydanticは、基本的にはPythonのデータモデル定義・データ検証・変換を担当します。FastAPIがそのPydanticモデルを利用することで、リクエストボディの解析やAPIドキュメント生成まで自動化してくれる、という関係です。

ここは今後かなり重要です。
PydanticがPOST通信をするわけではなく、受け取るデータの形やルールを定義・検証する。FastAPIがそのモデルをリクエストボディの定義として利用する。
Pydanticモデルを定義する

今回はモデル定義だけ行い、POST API自体はまだ作りません。
現在使っている app/main.py に、Pydanticモデルを追加します。
Pydanticの BaseModel をインポートします。
そして、app = FastAPI() より前にモデルを追加してください。
from fastapi import FastAPI
from pydantic import BaseModel
# 商品登録時に受け取るデータの構造を定義する
class ProductCreate(BaseModel):
name: str
price: int
description: str | None = None
app = FastAPI()
ProductCreate はPydanticの BaseModel を継承して作ります。
そして、以下によって受け取るデータの構造を定義しています。
name: str
price: int
description: str | None = None
ここで重要なのが必須・任意の違いです。
以下はデフォルト値がないため必須です。
name: str
price: int
一方、以下はデフォルト値として None が設定されているため、省略可能です。
description: str | None = None

ProductCreate という名前は、Pydanticで決められているわけではなく、こちらで自由に決められます。
今回は、
Product→ 商品Create→ 新規登録用
という意味で ProductCreate としています。
今後CRUDを作っていくと、以下のように用途ごとにモデルを分けることがあります。
ProductCreate
ProductUpdate
ProductResponse
POST APIを作成してJSONを受け取る

既存のGET APIはそのまま残して、main.py に次のPOST APIを追加してください。
@app.post("/products")
def create_product(product: ProductCreate):
# リクエストボディから受け取った商品データをそのまま返す
return product

今回のポイントは、product: ProductCreateです。
STEP 2では、product_id: intのような引数をFastAPIがパスパラメータなどとして解釈していました。
今回は型としてPydanticモデル ProductCreate を指定しています。
これをFastAPIが見ると、「product は ProductCreate の構造を持つリクエストボディとして受け取るんだな」と判断します。
つまり、クライアントから
{
"name": "Keyboard",
"price": 5000,
"description": "Mechanical keyboard"
}
が送られると、概念的には次のよう流れになります。
JSON
↓
FastAPI
↓
ProductCreateで検証・変換
↓
product
↓create_product()が実行される
そして関数内の product は単なるJSON文字列や辞書ではなく、ProductCreate のインスタンスになっています。
たとえば内部では、次のように値へアクセスできます。
product.name
product.price
product.description
Swagger UIからPOSTリクエストを送信する

Swagger UIに今回追加したPOST /productsが表示されているはずです。
Try it out を押し、Request body に次のJSONを入力します。
{
"name": "Keyboard",
"price": 5000,
"description": "Mechanical keyboard"
}
そのまま Execute を押してください。正常なら、レスポンスのステータスコードは200となり、Response bodyには入力した内容と同じデータが返ってきます。
実行結果↓

必須項目・型チェックなどPydanticのバリデーションを確認する

まずは、必須項目を省略した場合だけ確認しましょう。
現在のモデルでは、name と price にはデフォルト値がないため必須です。
Swagger UIから、今度は price を削除して送信してください。
実行結果↓

リクエストボディに必須であるpriceがないため、エラーになります。
今回の流れは以下のようになります。
リクエストを受信
↓ProductCreateの定義に従って入力値を検証
↓
必須のpriceが存在しない
↓
422エラーを返す
↓create_product()は実行されない

今度は型が間違っている場合を1つだけ確認しましょう。
price に整数へ変換できない文字列を指定してください。
リクエストボディ↓
{
"name": "Keyboard",
"price": "abc",
"description": "Mechanical keyboard"
}
実行結果↓

今回も以下にような流れでエラーが返ってきます。
price: intと定義
↓"abc"を受信
↓intに変換できない
↓
バリデーションエラー
↓create_product()は実行されない

今度は、任意項目の description を丸ごと省略して送ってみてください。
リクエストボディ↓
{
"name": "Keyboard",
"price": 5000
}
実行結果↓

descriptionは、description: str | None = Noneのようにデフォルト値を設定しているため、省略してもエラーになりません。
省略された場合は None が入ります。そしてJSONに変換してレスポンスすると、Pythonの None はJSONの null になります。
リクエストでdescriptionを省略
↓
デフォルト値None
↓ProductCreate.description = None
↓
JSONレスポンスではnull
Fieldを使った入力値の制約を追加する

これまでの内容で、priceは「整数であること」まではチェックできました。
ただこれだと、マイナスの価格も整数なので通ってしまいます。
そこでPydanticの Field を使うと、型だけでは表現できない追加ルールを設定できます。
まずimportを変更します。
from pydantic import BaseModel, Field
そして price を次のように変更します。
class ProductCreate(BaseModel):
name: str
price: int = Field(gt=0)
description: str | None = None
gt は greater than(より大きい) の意味です。つまり、price > 0という制約になります。
そのため、次のようになります。
price = 5000 # OK
price = 1 # OK
price = 0 # NG
price = -100 # NG
ここで大事なのは、int と Field の役割の違いです。
price: int = Field(gt=0)
この場合、以下のような役割分担になっています。
int → データ型の指定Field(gt=0) → その値に対する追加条件

Swagger UIから次のJSONを送ってください。
{
"name": "Keyboard",
"price": 0,
"description": "Mechanical keyboard"
}
実行結果↓

自力課題

今回の課題は、ユーザー登録用のPOST APIを作るです。
課題内容

次の仕様を満たすように実装してください。
エンドポイント:POST /users
リクエストボディ次の3項目です。
| 項目 | 型 | 条件 |
|---|---|---|
name | str | 必須 |
age | int | 必須、0より大きい |
email | str | 任意、省略時はNone |
※今回はメールアドレスの形式チェックまでは行いません。str として受け取れればOKです。
POST APIでは、受け取ったPydanticモデルをそのままレスポンスとして返すようにしてください。
完了条件

正常系として、以下を送信して200になること。
{
"name": "Taro",
"age": 30,
"email": "taro@example.com"
}
さらに、age に 0 を指定するとバリデーションエラーになることまで確認できれば課題完了です。
実装結果
実装したコード(実装箇所のみ抜粋)↓
class UserCreate(BaseModel):
name: str
age: int = Field(gt=0)
email: str | None = None
@app.post("/users")
def create_user(user: UserCreate):
# リクエストボディから受け取ったユーザーデータをそのまま返す
return user
正常系の結果
リクエストボディ
{
"name": "Taro",
"age": 30,
"email": "taro@example.com"
}
実行結果

age に 0を指定した結果

ChatGPT先生のレビュー結果

問題ありません。自力課題はクリアです。
コードも要件どおりです。
まとめ
今回はPOST APIとPydanticについて学びました。
POSTリクエストのリクエストボディの仕様は、PydanticのBaseModelを継承したクラスを作成して、そこに各項目の型を定義します。FastAPIはこの定義をもとに、リクエストボディの入力値を検証します。
この時にPydanticのField()を使用することで、さらに詳細な条件を指定することができます。
このクラスがそのままAPIのリクエストボディの仕様となり、Swagger UIにも自動で反映されるため、APIドキュメントのために別途実装が必要がないところがとても便利だと感じました。
コメント