Python | 3.11.2 |
---|---|
Pydantic | 2.3.0 |
FastAPI | 0.103.0 |
SQLAlchemy | 2.0.20 |
uvicorn | 0.23.2 |
dataclass
やTypeDict
といった組み込みライブラリとの親和性pip
でインストールできます。$ pip install pydantic
from datetime import datetime as dt
from pydantic import BaseModel, PositiveInt
class Order(BaseModel):
id: int
customer_name: str = 'ヤマダタロウ'
ordered_at: dt | None = dt.now()
price: PositiveInt = 1000
order_data = {'id': 111,
'customer_name': 'ヤマダハナコ',
'ordered_at': '2023-08-27 12:00:00',
'price': 2000}
order = Order(**order_data)
print(order.model_dump())
$ python app.py
{'id': 111, 'customer_name': 'ヤマダハナコ', 'ordered_at': datetime.datetime(2023, 8, 27, 12, 0), 'price': 2000}
id
部分を変更して文字列にして再実行みましょう。order_data = {'id': 'testID',
'customer_name': 'ヤマダハナコ',
'ordered_at': '2023-08-27 12:00:00',
'price': 2000}
pydantic_core._pydantic_core.ValidationError: 1 validation error for Order
Product
(商品)モデルを使うアプリケーションを作っていきます。fastapi
、uvicorn
、pydantic
、sqlalchemy
をインストールしましょう。$ pip install fastapi uvicorn pydantic pytest sqlalchemy
├── app.py # エントリーポイント
├── models # モデル
│ ├── base.py
│ └── product.py
├── schemas # リクエスト、レスポンスなどの定義
│ ├── __init__.py
│ └── product.py
└── services # ルーターと共にロジックを提供
└── product.py
models/
ディレクトリ配下にbase.py
を作りましょう。base.py
はベースモデルを定義します。今回はただsqlalchemy
のベース宣言クラスのDeclarativeBase
を継承するだけです。通常は、ここでid
やcreated_at
などの共通カラムの定義やデータベース接続を記載します。from sqlalchemy.orm import DeclarativeBase
class Base(DeclarativeBase):
pass
models/product.py
を見てみましょう。from sqlalchemy import Integer, String
from sqlalchemy.orm import Mapped, mapped_column
from pydantic import PositiveInt
from .base import Base
class Product(Base):
__tablename__ = "products"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
name: Mapped[str] = mapped_column(String(255), nullable=False)
price: Mapped[PositiveInt] = mapped_column(Integer, nullable=False)
description: Mapped[str] = mapped_column(String(255), nullable=True)
products
テーブルに「ID」「商品名」「価格」「商品説明」を定義しています。先ほど作ったbase.py
のBase
を継承しています。PydanticにBaseModel
が用意されていますが、sqlalchemy
と組み合わせるためここでは使用しません。schemas/
ディレクトリ以下のschemas/product.py
を以下のようにします。from pydantic import BaseModel, ConfigDict
class ProductBase(BaseModel):
name: str
price: int
description: str = None
class ProductCreate(ProductBase):
pass
class ProductResponse(ProductBase):
id: int
# 今回は省略しているが、from_attributesはmodel_validateを使う設定
model_config = ConfigDict(from_attributes=True)
ProductBase
で共通となるフィールドを定義しています。schemas
をロードしたら使えるようにするため、__init__.py
も以下のようにします。from .product import ProductCreate, ProductResponse
services/product.py
を以下のようにします。from fastapi import APIRouter
import schemas
from models.product import Product
router = APIRouter()
@router.get("/{id}", operation_id="get_product")
async def get_product(id: int) -> schemas.ProductResponse:
return Product(
id=id,
name=f"Product {id}",
price=100,
description=f"Description of product {id}",
)
@router.post("", operation_id="create_product")
async def create_product(product: schemas.ProductCreate) -> schemas.ProductResponse:
return Product(
id=2,
name=product.name,
price=product.price,
description=product.description,
)
app.py
を以下のように書きます。from fastapi import FastAPI
from services import product
app = FastAPI()
app.include_router(product.router, prefix="/product", tags=["product"])
[GET]/product/{id}
と[POST]/product
にルーターを設定できます。$ uvicorn app:app --reload --host 0.0.0.0 --port 8080
http://localhost/product/1
にブラウザでアクセスすると、以下のようなJSONが返ってきます。{
"name": "Product 1",
"price": 100,
"description": "Description of product 1",
"id": 1
}
curl
コマンドを使ってcreateのAPIにもアクセスしてみましょう。$ curl -X POST -H 'Content-type: application/json' --data '{"name":"Product2", "price": 200, "description": "Description of product 2"}' http://localhost:8080/product
{
"name": "Product2",
"price": 200,
"description": "Description of product 2",
"id": 2
}
name
フィールドは必須ですが、これを省いてリクエストしてみます。$ curl -X POST -H 'Content-type: application/json' --data '{"price": 200, "description": "Description of product 2"}' http://localhost:8080/product
{
"detail": [
{
"type": "missing",
"loc": [
"body",
"name"
],
"msg": "Field required",
"input": {
"price": 200,
"description": "Description of product 2"
},
"url": "https://errors.pydantic.dev/2.3/v/missing"
}
]
}
computed_field
は、@property
や@cached_property
デコレーターに関数の返り値を付与できます。from pydantic import BaseModel, computed_field
class Product(BaseModel):
unit_weight: int
quantity: int
@computed_field
@property
def total_weight(self) -> int:
return self.unit_weight * self.quantity
print(Product(unit_weight=10, quantity=5).model_dump())
# => {'unit_weight': 10, 'quantity': 5, 'total_weight': 50}
@validate_call
の紹介です。from pydantic import ValidationError, validate_call
@validate_call
def say_hello(name: str, age: int = 17) -> str:
return f"Hello, {name}! You are {age} years old."
print(say_hello("Tanabe", 34))
print(say_hello("Hanako"))
try:
print(say_hello("Kei", "Suke"))
except ValidationError as e:
print(e)
Hello, Tanabe! You are 34 years old.
Hello, Hanako! You are 17 years old.
1 validation error for say_hello
1
Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='Suke', input_type=str]
lax mode
(緩めモード)という状態です。言葉が意味する通り、整数値を文字列として渡しても自動で解釈してくれます。strict mode
で実装するべきです。from pydantic import ValidationError, BaseModel
class Product(BaseModel):
price: int
print(Product.model_validate({"price": '100'}))
try:
Product.model_validate({"price": '100'}, strict=True)
except ValidationError as e:
print(e)
price=100
1 validation error for Product
price
Input should be a valid integer [type=int_type, input_value='100', input_type=str]
pydantic-settings
があり、便利な設定値として定義できます。pip
でインストールしましょう。$ pip install pydantic-settings
from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict
import os
class MySQLSettings(BaseSettings):
host: str
port: int
user: str
password: str
database: str
class Settings(BaseSettings, case_sensitive=True):
api_key: str = Field('xxx', alias='app_api_key')
mysql: MySQLSettings
os.environ['mysql'] = '{"host": "localhost", "port": 3306, "user": "testuser", "password": "p@ssw0rd", "database": "testdb"}'
print(Settings().model_dump())
{
"api_key": "xxx",
"mysql": {
"host": "localhost",
"port": 3306,
"user": "testuser",
"password": "p@ssw0rd",
"database": "testdb"
}
}
ValidationError
に対するraiseで事足ります。try:
# Hoge
except ValidationError as e:
print(e)
# e.errors = 見つかったエラー一覧
# e.error_count = エラーの数
# e.json = JSON形式