はじめに
現在API開発の業務に携わっていて、APIのスキーマ定義にはPydanticを使っています。
実装を進めていく中で最近になって『この一行には、こういう意味があったのか...!』と、少しずつ理解が追いついてきました。
本記事では私がAPI開発に携わる中で便利だと感じた点、勉強になった点などを共有していきます。
目次
1. Pydanticスキーマの基本の形
はじめに、Pydanticスキーマの最も基本的な構造についてです。APIが受け取るデータやAPIが返すデータの「型」を定義するのがPydanticスキーマの役割です。 その「型」を定義するには、まずBaseModelをPydanticからインポートしてそれを継承したクラスを作ります。
from pydantic import BaseModel
#ItemSchemaという名前のスキーマを定義します
class ItemSchema(BaseModel):
item_id: int
item_name: str
price: float
is_available: bool
クラスの中に、扱いたいデータの項目を型ヒント(: int や : str)付きで書いていくだけで簡単に型チェックの設定することができます。
例えば、このItemSchemaを使うAPIにitem_idとして数字ではなく"abc"という文字列を送ります。
すると、私がエラー処理を何も書いていなくてもPydanticが自動で「item_idは整数じゃないとダメですよ」という内容のエラーを返してくれます。
「BaseModelを継承したクラスを作って、型ヒントを書く」
これだけで、基本的なデータ型のチェックはPydanticに任せられることが分かりました。
これが、私が理解したPydanticの基本の形です。
2. `Field`でもっと細かいルールを作ろう
基本的な型チェックは、前の章で書いたBaseModelと型ヒントだけで十分でした。
そんな時に使うのが、pydanticからインポートするFieldだと知りました。
Fieldを使うと、バリデーションのルールを簡単に書くことができます!ここでは私が実装しながら便利だな!と感じた機能についてご紹介します。
2-1. 必須項目と任意項目の書き分け
APIの項目には、「必ず送ってほしいもの(必須)」と「なくてもいいもの(任意)」があります。これをどうやってPydanticで表現するんだろう?と最初に疑問に思いました。
これはFieldの第一引数で簡単に設定できます。
from pydantic import BaseModel, Field
class ItemSchema(BaseModel):
item_id: int = Field(..., description="商品ID")
memo: str | None = Field(None, description="メモ")
「...」→ Fieldの第一引数に「...」を書くと、必須項目になる
「description」→ その項目がどんな値なのかを記入できる
「None」→ 型ヒントに| Noneを書いて、第一引数を「None」にすると任意項目になる
2-2. 数値のバリデーション
次に、数値の範囲指定です。
例えば、商品の個数(quantity)が0やマイナスで送られてきたら、計算がおかしくなってしまいます。
これもFieldの引数で、簡単に不備がないように設定することができました。
class ItemSchema(BaseModel):
# 1個以上、99個以下にしたい場合
quantity: int = Field(..., ge=1, le=99, description="数量")
ge/le → 1個以上99個以下にしたい場合はge=1(1以上)やle=99(99以下)のように書くことで、数値の範囲を制限できます。
ちなみにgt(より大きい)やlt(より小さい)という引数もあります。
2-3. 文字列のバリデーション
class UserSchema(BaseModel):
# ユーザー名は8文字以上、20文字以下にしたい場合
username: str = Field(..., min_length=8, max_length=20, description="ユーザー名")
# 郵便番号("123-4567"のような形式)にしたい場合
zip_code: str = Field(..., pattern=r"^\d{3}-\d{4}$", description="郵便番号")
min_length/max_length → ユーザー名は8文字以上20文字以下にしたい場合、このように引数を指定します。
pattern → 郵便番号("123-4567"のような形式)にしたい場合、patternを書くことで独自のフォーマットをチェックできます。
補足:
patternに書かれている r"^\\d{3}-\\d{4}$" という文字列は正規表現と呼ばれる書き方で、文字列のルールを表現します。見慣れない記号や数字が並んでいて少し分かりにくいですよね。この意味を調べてみたので、各記号の役割をメモしておきます。
-
^→ 「ここから始まる」という行の先頭を意味します。 -
\d→ 「数字1文字」を意味します。 -
{3}→ 直前の文字(今回は\d)が「3回繰り返す」ことを意味します。つまり\d{3}で「3桁の数字」になります。 -
-→ 「ハイフン」そのものです。 -
\d{4}→ 同じように、「4桁の数字」を意味します。 -
$→ 「ここで終わる」という行の末尾を意味します。
これらのパーツを組み合わせることで、行の先頭から3桁の数字/ハイフン/4桁の数字で終わりという一つのルールが出来上がります。
3. スキーマを組み合わせてもっと複雑なデータを作る
これまではitem_idやitem_nameのような単一の項目を定義する方法を見てきました。
しかし、実際のAPI開発では一覧を取得することが多くあるかと思います。
その際、「各書籍の情報にどのストアが販売しているのかも添えて表示したい」といったより複雑なデータを扱う場面がたくさんあります。
例えば、APIが最終的に返したいJSONの形が、このようになっているケースを考えてみましょう。
[
{
"item_id": 123,
"item_name": "実践Pydantic入門",
"seller": {
"user_id": 987,
"username": "TechBookストア"
}
},
{
"item_id": 124,
"item_name": "FastAPI完全ガイド",
"seller": {
"user_id": 987,
"username": "TechBookストア"
}
}
]
このJSONは、全体がリスト[]で囲まれており、その中に複数の「書籍情報」オブジェクト{}が入っています。
そして一番のポイントは、各書籍情報の中にさらにsellerというキーで「販売ストア情報」のオブジェクトが入れ子(ネスト)になっている点です。
この「入れ子構造」をPydanticではどうやって安全に、そして綺麗に表現するのかをPythonのコードで見ていきましょう。
from pydantic import BaseModel, Field
from typing import List
class SellerSchema(BaseModel):
user_id: int = Field(..., description="ストアのID")
username: str = Field(..., description="ストア名")
class BookResponseSchema(BaseModel):
item_id: int = Field(..., description="書籍のID")
item_name: str = Field(..., description="書籍名")
seller: SellerSchema
まず、JSONの入れ子の内側部分である「販売ストア情報」のスキーマを
class SellerSchema(BaseModel):で独立したクラスとして定義します。
これを後で使う「部品」として準備しておくイメージです。user_idとusernameはそれぞれ必須項目ということが分かりますね。
次に、JSONの外側部分である「書籍情報」をclass BookResponseSchema(BaseModel):として定義します。その中で、sellerというフィールドを作ります。
そしてここが一番のポイントですが、その型ヒントに先ほど「部品」として作ったSellerSchemaクラスそのものを指定します。
これにより、BookResponseSchemaの中のsellerは、SellerSchemaの形をしていなければならないというスキーマ同士の親子関係が出来上がります。
最後に、APIのエンドポイントで戻り値のモデルをList[BookResponseSchema]と指定することで、JSON全体が[]で囲まれたリスト形式であることを表現します。
このように、Pydanticではまず小さな部品(SellerSchema)を作りそれを大きな部品(BookResponseSchema)に組み込むという流れでどんなに複雑なJSON構造でも分かりやすく表現することができるのです。
4. まとめ
本記事では、私がAPI開発を通して学んだPydanticの基本的な使い方を3つのポイントに絞って紹介しました。
- 基本的なバリデーション(型チェック)
-
Fieldを使ったより細かいルールの設定 - スキーマを組み合わせた複雑なデータ構造の表現
最初はAPIのスキーマを定義するためのお作法のようなものだと感じていましたが、学習を進めるうちにPydanticが単なるバリデーションツールではなく、APIの品質と開発効率を向上させてくれる、強力な設計ツールだと理解しました。
最後に、もし「Pydanticがなかったらどうなるのか」を本記事で紹介した数値の範囲チェックや文字列のフォーマットチェックを例に見てみましょう。
これらをもしPydanticなしで実装しようとすると、私たちはAPIのロジックの中にこのようなコードを延々と書くことになります。
def process_data(data: dict):
# 型チェック
if not isinstance(data.get("quantity"), int):
raise TypeError("quantityは整数である必要があります。")
# 範囲チェック
if not (1 <= data["quantity"] <= 99):
raise ValueError("quantityは1から99の間でなければなりません。")
#...
Pydanticを使用しない場合は手動でチェックする必要があり、他の項目についてもこのようなコードが延々と続きます。
この手作業でのチェックはコードを読みにくくするだけでなく、チェック漏れといったバグの温床にもなりがちです。
Pydanticがあれば、これらの間違いやすい記述をField(ge=1, le=99)のようなたった一行の宣言で置き換えることができます。
私もまだまだ勉強中ですが、この記事がPydanticを学び始めた方にとって何かのヒントになれば幸いです。
最後までお読みいただき、ありがとうございました!