pydantic
PydanticのRust製コアによる高速バリデーション機能を活用し、型ヒントとランタイム型チェックを用いたPythonデータ検証を行います。FastAPI・Djangoとの連携やコンフィグ管理など、型安全が求められる場面で活躍します。
description の原文を見る
Python data validation using type hints and runtime type checking with Pydantic v2's Rust-powered core for high-performance validation in FastAPI, Django, and configuration management.
SKILL.md 本文
Pydantic 検証スキル
概要
型ヒントと実行時型チェックを使用した Python データ検証。Pydantic v2 の Rust 駆動コアにより高性能検証を実現します。
使用場面
- API リクエスト/レスポンス検証 (FastAPI、Django)
- 設定と構成管理 (環境変数、設定ファイル)
- ORM モデル検証 (SQLAlchemy 統合)
- データ解析とシリアライゼーション (JSON、辞書、カスタム形式)
- 自動検証機能付きの型安全なデータクラス
- 型安全性を備えた CLI 引数解析
クイックスタート
from pydantic import BaseModel, Field, EmailStr
from datetime import datetime
class User(BaseModel):
id: int
name: str = Field(..., min_length=1, max_length=100)
email: EmailStr
created_at: datetime = Field(default_factory=datetime.now)
is_active: bool = True
# データの検証
user = User(id=1, name="Alice", email="alice@example.com")
print(user.model_dump()) # {'id': 1, 'name': 'Alice', ...}
# 自動型変換
user2 = User(id="2", name="Bob", email="bob@example.com")
assert user2.id == 2 # 文字列 "2" が int に変換される
# 検証エラー
try:
User(id=3, name="", email="invalid")
except ValidationError as e:
print(e.errors())
コア概念
BaseModel の基礎
from pydantic import BaseModel, ConfigDict
class Product(BaseModel):
model_config = ConfigDict(
str_strip_whitespace=True,
validate_assignment=True,
use_enum_values=True,
arbitrary_types_allowed=False
)
name: str
price: float
quantity: int = 0
# 使用例
product = Product(name=" Widget ", price=19.99)
assert product.name == "Widget" # 空白が削除される
# 割り当て時に検証
product.price = "29.99" # 自動的に float に変換
フィールド設定
from pydantic import Field, field_validator
from typing import Annotated
class Item(BaseModel):
# フィールド制約
sku: str = Field(pattern=r'^[A-Z]{3}-\d{4}$')
price: float = Field(gt=0, le=10000)
stock: int = Field(ge=0, default=0)
# Annotated 型 (Pydantic v2)
quantity: Annotated[int, Field(ge=1, le=100)]
# 説明と例
description: str = Field(
...,
description="Product description",
examples=["High-quality widget"]
)
# 廃止予定フィールド
old_field: str | None = Field(None, deprecated=True)
@field_validator('sku')
@classmethod
def validate_sku(cls, v: str) -> str:
if not v.startswith('ABC'):
raise ValueError('SKU must start with ABC')
return v
Pydantic v2 の改善点
v1 からのマイグレーション
# Pydantic v1
class OldModel(BaseModel):
class Config:
validate_assignment = True
json_encoders = {datetime: lambda v: v.isoformat()}
# Pydantic v2
class NewModel(BaseModel):
model_config = ConfigDict(
validate_assignment=True,
# json_encoders はシリアライザーに置き換わり
)
@model_serializer
def ser_model(self) -> dict:
return {...}
# 主な変更:
# - .dict() → .model_dump()
# - .json() → .model_dump_json()
# - .parse_obj() → .model_validate()
# - .parse_raw() → .model_validate_json()
# - @validator → @field_validator
# - @root_validator → @model_validator
パフォーマンスの改善
# v2 は Rust コア (pydantic-core) を使用して 5~50 倍高速化
from pydantic import BaseModel
import time
class Data(BaseModel):
values: list[int]
names: list[str]
# ベンチマーク
data = {'values': list(range(10000)), 'names': ['item'] * 10000}
start = time.perf_counter()
for _ in range(1000):
Data.model_validate(data)
elapsed = time.perf_counter() - start
print(f"Validated 1000 iterations in {elapsed:.2f}s")
フィールド型
組み込み型
from pydantic import (
BaseModel, EmailStr, HttpUrl, UUID4,
FilePath, DirectoryPath, Json, SecretStr,
PositiveInt, NegativeFloat, conint, constr
)
from typing import Literal
from pathlib import Path
class Example(BaseModel):
# メール検証
email: EmailStr
# URL 検証
website: HttpUrl
# UUID
id: UUID4
# ファイルシステムパス
config_file: FilePath
data_dir: DirectoryPath
# JSON 文字列 → パースされたオブジェクト
metadata: Json[dict[str, str]]
# シークレット (ログに出力されない)
api_key: SecretStr
# 制約型
age: PositiveInt
balance: NegativeFloat
username: constr(min_length=3, max_length=20, pattern=r'^[a-z]+$')
code: conint(ge=1000, le=9999)
# リテラル型
status: Literal['pending', 'approved', 'rejected']
カスタム型
from pydantic import GetCoreSchemaHandler, GetJsonSchemaHandler
from pydantic_core import core_schema
from typing import Any
class Color:
def __init__(self, r: int, g: int, b: int):
self.r, self.g, self.b = r, g, b
@classmethod
def __get_pydantic_core_schema__(
cls, source_type: Any, handler: GetCoreSchemaHandler
) -> core_schema.CoreSchema:
return core_schema.no_info_after_validator_function(
cls.validate,
core_schema.str_schema()
)
@classmethod
def validate(cls, v: str) -> 'Color':
if not v.startswith('#') or len(v) != 7:
raise ValueError('Invalid hex color')
r = int(v[1:3], 16)
g = int(v[3:5], 16)
b = int(v[5:7], 16)
return cls(r, g, b)
class Design(BaseModel):
primary_color: Color
# 使用例
design = Design(primary_color='#FF5733')
assert design.primary_color.r == 255
バリデーター
フィールドバリデーター
from pydantic import field_validator, model_validator
class Account(BaseModel):
username: str
password: str
password_confirm: str
@field_validator('username')
@classmethod
def username_alphanumeric(cls, v: str) -> str:
if not v.isalnum():
raise ValueError('must be alphanumeric')
return v
@field_validator('password')
@classmethod
def password_strong(cls, v: str) -> str:
if len(v) < 8:
raise ValueError('must be at least 8 characters')
if not any(c.isupper() for c in v):
raise ValueError('must contain uppercase letter')
return v
# 複数フィールドの検証
@field_validator('username', 'password')
@classmethod
def not_empty(cls, v: str) -> str:
if not v or not v.strip():
raise ValueError('must not be empty')
return v.strip()
モデルバリデーター
from pydantic import model_validator
from typing import Self
class DateRange(BaseModel):
start_date: datetime
end_date: datetime
@model_validator(mode='after')
def check_dates(self) -> Self:
if self.end_date < self.start_date:
raise ValueError('end_date must be after start_date')
return self
class Order(BaseModel):
items: list[str]
total: float
discount: float = 0
@model_validator(mode='before')
@classmethod
def calculate_total(cls, data: dict) -> dict:
# 検証前の前処理
if isinstance(data, dict) and 'total' not in data:
data['total'] = len(data.get('items', [])) * 10.0
return data
ルートバリデーター (Wrap)
from pydantic import model_validator, ValidationInfo
class Config(BaseModel):
env: Literal['dev', 'prod']
debug: bool = False
@model_validator(mode='wrap')
@classmethod
def validate_config(cls, values: Any, handler, info: ValidationInfo):
# デフォルト検証を実行
result = handler(values)
# 検証後のロジック
if result.env == 'prod' and result.debug:
raise ValueError('debug cannot be True in production')
return result
型変換と Strict モード
from pydantic import BaseModel, ConfigDict, ValidationError
# 型変換モード (デフォルト)
class CoerciveModel(BaseModel):
count: int
price: float
data = CoerciveModel(count="42", price="19.99")
assert data.count == 42 # 文字列 → int
assert data.price == 19.99 # 文字列 → float
# Strict モード
class StrictModel(BaseModel):
model_config = ConfigDict(strict=True)
count: int
price: float
try:
StrictModel(count="42", price="19.99") # ValidationError が発生
except ValidationError as e:
print("Strict mode: no coercion allowed")
# フィールド単位の strict モード
class MixedModel(BaseModel):
flexible: int # 型変換を許可
strict: Annotated[int, Field(strict=True)] # 型変換を不許可
MixedModel(flexible="1", strict=2) # OK
# MixedModel(flexible="1", strict="2") # ValidationError
ネストされたモデルと再帰的型
from pydantic import BaseModel
from typing import ForwardRef
# ネストされたモデル
class Address(BaseModel):
street: str
city: str
country: str
class Company(BaseModel):
name: str
address: Address
company = Company(
name="ACME Corp",
address={'street': '123 Main St', 'city': 'NYC', 'country': 'USA'}
)
# 再帰的型 (ツリー構造)
class TreeNode(BaseModel):
value: int
children: list['TreeNode'] = []
TreeNode.model_rebuild() # 前方参照に必要
tree = TreeNode(
value=1,
children=[
TreeNode(value=2, children=[TreeNode(value=4)]),
TreeNode(value=3)
]
)
# Self 参照を持つ ForwardRef
class Category(BaseModel):
name: str
parent: 'Category | None' = None
subcategories: list['Category'] = []
Category.model_rebuild()
ジェネリックモデル
from pydantic import BaseModel
from typing import Generic, TypeVar
T = TypeVar('T')
class Response(BaseModel, Generic[T]):
success: bool
data: T
message: str = ''
class User(BaseModel):
id: int
name: str
# 具体的な型での使用
user_response = Response[User](
success=True,
data=User(id=1, name='Alice')
)
# リスト応答
list_response = Response[list[User]](
success=True,
data=[User(id=1, name='Alice'), User(id=2, name='Bob')]
)
# ジェネリックリポジトリパターン
class Repository(BaseModel, Generic[T]):
items: list[T]
def add(self, item: T) -> None:
self.items.append(item)
user_repo = Repository[User](items=[])
user_repo.add(User(id=1, name='Alice'))
シリアライゼーション
Model Dump
from pydantic import BaseModel, Field, field_serializer
class Article(BaseModel):
title: str
content: str
tags: list[str]
metadata: dict[str, Any] = {}
# シリアライゼーションのカスタマイズ
@field_serializer('tags')
def serialize_tags(self, tags: list[str]) -> str:
return ','.join(tags)
article = Article(
title='Pydantic Guide',
content='...',
tags=['python', 'validation']
)
# 辞書にダンプ
data = article.model_dump()
# {'title': 'Pydantic Guide', 'tags': 'python,validation', ...}
# フィールドを除外
data = article.model_dump(exclude={'metadata'})
# 特定フィールドのみ含める
data = article.model_dump(include={'title', 'tags'})
# 未設定フィールドを除外
article2 = Article(title='Test', content='...', tags=[])
data = article2.model_dump(exclude_unset=True) # metadata は除外される
# エイリアスで出力
class AliasModel(BaseModel):
internal_name: str = Field(alias='externalName')
model = AliasModel(externalName='value')
model.model_dump(by_alias=True) # {'externalName': 'value'}
JSON シリアライゼーション
from datetime import datetime
from pydantic import BaseModel, field_serializer
class Event(BaseModel):
name: str
timestamp: datetime
@field_serializer('timestamp')
def serialize_dt(self, dt: datetime) -> str:
return dt.isoformat()
event = Event(name='Deploy', timestamp=datetime.now())
# JSON 文字列にダンプ
json_str = event.model_dump_json()
# '{"name":"Deploy","timestamp":"2025-11-30T..."}'
# 見栄え良く出力
json_str = event.model_dump_json(indent=2)
# JSON からパース
event2 = Event.model_validate_json(json_str)
カスタムシリアライザー
from pydantic import model_serializer
class User(BaseModel):
id: int
username: str
password: SecretStr
@model_serializer
def ser_model(self) -> dict[str, Any]:
return {
'id': self.id,
'username': self.username,
# パスワードは絶対にシリアライズしない
}
user = User(id=1, username='alice', password='secret123')
assert 'password' not in user.model_dump()
設定管理
BaseSettings
from pydantic_settings import BaseSettings, SettingsConfigDict
from pydantic import Field
class AppSettings(BaseSettings):
model_config = SettingsConfigDict(
env_file='.env',
env_file_encoding='utf-8',
env_prefix='APP_',
case_sensitive=False
)
# 環境変数
database_url: str
redis_url: str = 'redis://localhost:6379'
secret_key: SecretStr
debug: bool = False
# ネストされた設定
class SMTPSettings(BaseModel):
host: str
port: int = 587
username: str
password: SecretStr
smtp: SMTPSettings
# 環境変数から読み込み:
# APP_DATABASE_URL, APP_REDIS_URL, APP_SECRET_KEY, APP_DEBUG
# APP_SMTP__HOST, APP_SMTP__PORT, など
settings = AppSettings()
マルチ環境設定
from functools import lru_cache
class Settings(BaseSettings):
environment: Literal['dev', 'staging', 'prod'] = 'dev'
database_url: str
api_key: SecretStr
model_config = SettingsConfigDict(
env_file='.env',
extra='ignore'
)
@property
def is_production(self) -> bool:
return self.environment == 'prod'
@lru_cache
def get_settings() -> Settings:
return Settings()
# FastAPI での使用
from fastapi import Depends
@app.get('/config')
def get_config(settings: Settings = Depends(get_settings)):
return {'env': settings.environment}
FastAPI 統合
リクエスト/レスポンスモデル
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, EmailStr
app = FastAPI()
class UserCreate(BaseModel):
username: str = Field(min_length=3, max_length=50)
email: EmailStr
password: str = Field(min_length=8)
class UserResponse(BaseModel):
id: int
username: str
email: EmailStr
model_config = ConfigDict(from_attributes=True)
@app.post('/users', response_model=UserResponse)
def create_user(user: UserCreate):
# FastAPI がリクエストボディを自動検証
# レスポンスは UserResponse のフィールドのみを返す (password は除外)
return UserResponse(
id=1,
username=user.username,
email=user.email
)
クエリパラメータ
from pydantic import BaseModel, Field
from fastapi import Query
class PaginationParams(BaseModel):
skip: int = Field(0, ge=0)
limit: int = Field(10, ge=1, le=100)
class SearchParams(BaseModel):
q: str = Field(..., min_length=1)
category: str | None = None
sort_by: Literal['date', 'relevance'] = 'relevance'
@app.get('/search')
def search(params: SearchParams = Query()):
return {'query': params.q, 'sort': params.sort_by}
レスポンスモデルのカスタマイズ
class DetailedUser(BaseModel):
id: int
username: str
email: EmailStr
created_at: datetime
last_login: datetime | None
@app.get('/users/{user_id}', response_model=DetailedUser)
def get_user(user_id: int, include_dates: bool = False):
user = DetailedUser(
id=user_id,
username='alice',
email='alice@example.com',
created_at=datetime.now(),
last_login=None
)
if not include_dates:
return user.model_dump(exclude={'created_at', 'last_login'})
return user
SQLAlchemy 統合
ORM モデルと Pydantic
from sqlalchemy import Column, Integer, String, DateTime
from sqlalchemy.orm import DeclarativeBase
from pydantic import BaseModel, ConfigDict
class Base(DeclarativeBase):
pass
# SQLAlchemy ORM モデル
class UserDB(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
username = Column(String(50), unique=True)
email = Column(String(100))
created_at = Column(DateTime, default=datetime.utcnow)
# 検証用 Pydantic モデル
class UserSchema(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
username: str
email: EmailStr
created_at: datetime
# 使用例
from sqlalchemy.orm import Session
def get_user(db: Session, user_id: int) -> UserSchema:
user = db.query(UserDB).filter(UserDB.id == user_id).first()
return UserSchema.model_validate(user) # ORM → Pydantic
ハイブリッドアプローチ
from pydantic import BaseModel
class UserBase(BaseModel):
username: str
email: EmailStr
class UserCreate(UserBase):
password: str
class UserUpdate(BaseModel):
username: str | None = None
email: EmailStr | None = None
password: str | None = None
class UserInDB(UserBase):
model_config = ConfigDict(from_attributes=True)
id: int
created_at: datetime
password_hash: str
# CRUD 操作
def create_user(db: Session, user: UserCreate) -> UserInDB:
db_user = UserDB(
username=user.username,
email=user.email,
password_hash=hash_password(user.password)
)
db.add(db_user)
db.commit()
db.refresh(db_user)
return UserInDB.model_validate(db_user)
Django 統合
Django モデルの検証
from django.db import models
from pydantic import BaseModel, field_validator
# Django モデル
class Article(models.Model):
title = models.CharField(max_length=200)
content = models.TextField()
published = models.BooleanField(default=False)
# Pydantic スキーマ
class ArticleSchema(BaseModel):
model_config = ConfigDict(from_attributes=True)
title: str = Field(max_length=200)
content: str
published: bool = False
@field_validator('content')
@classmethod
def validate_content(cls, v: str) -> str:
if len(v) < 100:
raise ValueError('Content too short')
return v
# Django ビューでの使用
from django.http import JsonResponse
from django.views.decorators.http import require_http_methods
@require_http_methods(['POST'])
def create_article(request):
try:
data = ArticleSchema.model_validate_json(request.body)
article = Article.objects.create(**data.model_dump())
return JsonResponse({'id': article.id})
except ValidationError as e:
return JsonResponse({'errors': e.errors()}, status=400)
計算フィールド
from pydantic import computed_field
class Rectangle(BaseModel):
width: float
height: float
@computed_field
@property
def area(self) -> float:
return self.width * self.height
@computed_field
@property
def perimeter(self) -> float:
return 2 * (self.width + self.height)
rect = Rectangle(width=10, height=5)
assert rect.area == 50
assert rect.perimeter == 30
# シリアライゼーション時の計算フィールド
data = rect.model_dump()
# {'width': 10.0, 'height': 5.0, 'area': 50.0, 'perimeter': 30.0}
カスタムエラー
from pydantic import BaseModel, field_validator, ValidationError
from pydantic_core import PydanticCustomError
class StrictUser(BaseModel):
username: str
age: int
@field_validator('username')
@classmethod
def validate_username(cls, v: str) -> str:
if len(v) < 3:
raise PydanticCustomError(
'username_too_short',
'Username must be at least 3 characters',
{'min_length': 3, 'actual_length': len(v)}
)
return v
@field_validator('age')
@classmethod
def validate_age(cls, v: int) -> int:
if v < 18:
raise PydanticCustomError(
'underage',
'User must be at least 18 years old',
{'age': v, 'minimum_age': 18}
)
return v
# カスタムエラー処理
try:
StrictUser(username='ab', age=16)
except ValidationError as e:
for error in e.errors():
print(f"{error['type']}: {error['msg']}")
print(f"Context: {error.get('ctx')}")
パフォーマンス最適化
v2 Rust コアの利点
# Pydantic v2 は pydantic-core (Rust) を使用:
# - 5~50 倍高速な検証
# - メモリ使用量の削減
# - 改善されたエラーメッセージ
# - 改善された JSON パース
import timeit
from pydantic import BaseModel
class Data(BaseModel):
values: list[int]
names: list[str]
metadata: dict[str, Any]
# ベンチマーク
data_dict = {
'values': list(range(1000)),
'names': ['item'] * 1000,
'metadata': {'key': 'value'}
}
def validate():
Data.model_validate(data_dict)
time_taken = timeit.timeit(validate, number=10000)
print(f"10000 validations: {time_taken:.2f}s")
最適化テクニック
from pydantic import BaseModel, ConfigDict
class OptimizedModel(BaseModel):
model_config = ConfigDict(
# 必要な場合のみ割り当て時に検証
validate_assignment=False,
# 内部使用時は検証を無効化
validate_default=False,
# メモリ効率のためのスロット使用
# (Pydantic v2 BaseModel では直接利用不可)
)
data: list[int]
# バリデーターの再利用
from functools import lru_cache
@lru_cache(maxsize=128)
def get_validator(model_class):
return model_class.model_validate
# 一括検証
def validate_bulk(items: list[dict]) -> list[Data]:
validator = get_validator(Data)
return [validator(item) for item in items]
JSON Schema の生成
from pydantic import BaseModel, Field
class Product(BaseModel):
"""Product model for catalog"""
id: int = Field(description="Unique product identifier")
name: str = Field(description="Product name", examples=["Widget"])
price: float = Field(gt=0, description="Price in USD")
tags: list[str] = Field(default=[], description="Product tags")
# JSON Schema の生成
schema = Product.model_json_schema()
print(json.dumps(schema, indent=2))
# {
# "title": "Product",
# "description": "Product model for catalog",
# "type": "object",
# "properties": {
# "id": {"type": "integer", "description": "Unique product identifier"},
# "name": {"type": "string", "description": "Product name"},
# ...
# },
# "required": ["id", "name", "price"]
# }
# OpenAPI 互換
from fastapi import FastAPI
app = FastAPI()
@app.post('/products')
def create_product(product: Product):
return product
# FastAPI は Pydantic モデルから自動的に OpenAPI schema を生成
データクラス統合
from pydantic.dataclasses import dataclass
from pydantic import Field
@dataclass
class User:
id: int
name: str = Field(min_length=1)
email: str = Field(pattern=r'.+@.+\..+')
# Pydantic BaseModel のように検証機能を備えて動作
user = User(id=1, name='Alice', email='alice@example.com')
# 構築時の検証
try:
User(id=2, name='', email='invalid')
except ValidationError as e:
print(e.errors())
# Pydantic BaseModel に変換
from pydantic import BaseModel
class UserModel(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
name: str
email: str
user_model = UserModel.model_validate(user)
テスト戦略
ユニットテストモデル
import pytest
from pydantic import ValidationError
def test_user_validation():
# 有効なデータ
user = User(id=1, name='Alice', email='alice@example.com')
assert user.name == 'Alice'
# 無効なデータ
with pytest.raises(ValidationError) as exc_info:
User(id='invalid', name='Bob', email='bob@example.com')
errors = exc_info.value.errors()
assert errors[0]['type'] == 'int_parsing'
def test_user_serialization():
user = User(id=1, name='Alice', email='alice@example.com')
data = user.model_dump()
assert data == {
'id': 1,
'name': 'Alice',
'email': 'alice@example.com'
}
def test_nested_validation():
company = Company(
name='ACME',
address={'street': '123 Main', 'city': 'NYC', 'country': 'USA'}
)
assert company.address.city == 'NYC'
フィクスチャを使用したテスト
@pytest.fixture
def sample_user_data():
return {
'id': 1,
'name': 'Alice',
'email': 'alice@example.com'
}
@pytest.fixture
def sample_user(sample_user_data):
return User(**sample_user_data)
def test_with_fixtures(sample_user):
assert sample_user.name == 'Alice'
def test_invalid_email(sample_user_data):
sample_user_data['email'] = 'invalid'
with pytest.raises(ValidationError):
User(**sample_user_data)
プロパティベースのテスト
from hypothesis import given, strategies as st
@given(
id=st.integers(min_value=1),
name=st.text(min_size=1, max_size=100),
email=st.emails()
)
def test_user_always_valid(id, name, email):
user = User(id=id, name=name, email=email)
assert user.id == id
assert user.name == name
assert user.email == email
マイグレーションガイド (v1 → v2)
主な変更点
# v1
from pydantic import BaseModel
class OldModel(BaseModel):
class Config:
validate_assignment = True
arbitrary_types_allowed = True
# バリデーター
@validator('field')
def validate_field(cls, v):
return v
@root_validator
def validate_model(cls, values):
return values
# シリアライゼーション
data = model.dict()
json_str = model.json()
# パース
model = OldModel.parse_obj(data)
model = OldModel.parse_raw(json_str)
# v2
from pydantic import BaseModel, ConfigDict, field_validator, model_validator
class NewModel(BaseModel):
model_config = ConfigDict(
validate_assignment=True,
arbitrary_types_allowed=True
)
# フィールドバリデーター
@field_validator('field')
@classmethod
def validate_field(cls, v):
return v
# モデルバリデーター
@model_validator(mode='after')
def validate_model(self):
return self
# シリアライゼーション
data = model.model_dump()
json_str = model.model_dump_json()
# パース
model = NewModel.model_validate(data)
model = NewModel.model_validate_json(json_str)
マイグレーションチェックリスト
-
class Configをmodel_config = ConfigDict()に置き換える -
.dict()を.model_dump()に更新 -
.json()を.model_dump_json()に更新 -
.parse_obj()を.model_validate()に更新 -
.parse_raw()を.model_validate_json()に更新 -
@validatorを@field_validatorと@classmethodに更新 -
@root_validatorを@model_validator(mode='after')に更新 -
json_encodersを@field_serializerに置き換える - strict モード動作の変更を確認
- カスタム型を
__get_pydantic_core_schema__を使用するよう更新
ベストプラクティス
モデル構成
# 用途別にスキーマを分離
class UserBase(BaseModel):
"""共有フィールド"""
username: str
email: EmailStr
class UserCreate(UserBase):
"""ユーザー作成用 API リクエスト"""
password: str
class UserUpdate(BaseModel):
"""ユーザー更新用 API リクエスト (すべてオプション)"""
username: str | None = None
email: EmailStr | None = None
password: str | None = None
class UserInDB(UserBase):
"""データベース表現"""
model_config = ConfigDict(from_attributes=True)
id: int
password_hash: str
created_at: datetime
class UserResponse(UserBase):
"""API レスポンス (機密データを除外)"""
id: int
created_at: datetime
検証のベストプラクティス
# バリデーターではなく Field で制約を使用
class Good(BaseModel):
age: int = Field(ge=0, le=150)
email: EmailStr
class Bad(BaseModel):
age: int
email: str
@field_validator('age')
@classmethod
def validate_age(cls, v):
if v < 0 or v > 150:
raise ValueError('invalid age')
return v
# 継承ではなく構成を優先
class TimestampMixin(BaseModel):
created_at: datetime = Field(default_factory=datetime.utcnow)
updated_at: datetime = Field(default_factory=datetime.utcnow)
class User(TimestampMixin):
username: str
email: EmailStr
エラー処理
from pydantic import ValidationError
def safe_validate(data: dict) -> User | None:
try:
return User.model_validate(data)
except ValidationError as e:
# 検証エラーをログに記録
logger.error(f"Validation failed: {e.errors()}")
return None
def validate_with_details(data: dict):
try:
return User.model_validate(data)
except ValidationError as e:
# ユーザーフレンドリーなエラーを返す
return {
'success': False,
'errors': [
{
'field': '.'.join(str(loc) for loc in err['loc']),
'message': err['msg'],
'type': err['type']
}
for err in e.errors()
]
}
一般的なパターン
API レスポンスラッパー
from typing import Generic, TypeVar
T = TypeVar('T')
class APIResponse(BaseModel, Generic[T]):
success: bool
data: T | None = None
error: str | None = None
metadata: dict[str, Any] = {}
# 使用例
user_response = APIResponse[User](
success=True,
data=User(id=1, name='Alice', email='alice@example.com')
)
error_response = APIResponse[User](
success=False,
error='User not found'
)
ページネーション
class PaginatedResponse(BaseModel, Generic[T]):
items: list[T]
total: int
page: int
page_size: int
@computed_field
@property
def total_pages(self) -> int:
return (self.total + self.page_size - 1) // self.page_size
users = PaginatedResponse[User](
items=[...],
total=100,
page=1,
page_size=10
)
assert users.total_pages == 10
監査フィールド
class AuditMixin(BaseModel):
created_at: datetime = Field(default_factory=datetime.utcnow)
updated_at: datetime = Field(default_factory=datetime.utcnow)
created_by: int | None = None
updated_by: int | None = None
class Document(AuditMixin):
title: str
content: str
@model_validator(mode='before')
@classmethod
def update_timestamp(cls, data: dict) -> dict:
if isinstance(data, dict):
data['updated_at'] = datetime.utcnow()
return data
関連スキル
Pydantic を使用する際は、以下の補完的なスキルを検討してください:
- fastapi-local-dev: Pydantic 統合を備えた FastAPI 開発サーバーパターン
- sqlalchemy: Pydantic 検証を備えたデータベースモデルの SQLAlchemy ORM パターン
- django: Pydantic スキーマを備えた Django フレームワーク統合
- pytest: Pydantic モデルと検証のテスト戦略
FastAPI 統合クイックリファレンス (スタンドアロン使用用)
# FastAPI と Pydantic (基本パターン)
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, EmailStr
app = FastAPI()
class UserCreate(BaseModel):
username: str
email: EmailStr
password: str
class UserResponse(BaseModel):
id: int
username: str
email: EmailStr
model_config = ConfigDict(from_attributes=True)
@app.post('/users', response_model=UserResponse)
def create_user(user: UserCreate):
# FastAPI は Pydantic を使用して自動検証
# response_model はパスワードをフィルタリング
return UserResponse(id=1, username=user.username, email=user.email)
SQLAlchemy 統合クイックリファレンス (スタンドアロン使用用)
# SQLAlchemy 2.0 と Pydantic 検証
from sqlalchemy import Column, Integer, String
from sqlalchemy.orm import DeclarativeBase
from pydantic import BaseModel, ConfigDict
class Base(DeclarativeBase):
pass
class UserDB(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
username = Column(String(50))
email = Column(String(100))
class UserSchema(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
username: str
email: str
# ORM を Pydantic に変換
user_orm = db.query(UserDB).first()
user_validated = UserSchema.model_validate(user_orm)
Pytest テストクイックリファレンス (スタンドアロン使用用)
# pytest を使用した Pydantic モデルのテスト
import pytest
from pydantic import ValidationError
def test_user_validation():
user = User(id=1, name='Alice', email='alice@example.com')
assert user.name == 'Alice'
def test_validation_error():
with pytest.raises(ValidationError) as exc_info:
User(id='invalid', name='Bob', email='bob@example.com')
errors = exc_info.value.errors()
assert errors[0]['type'] == 'int_parsing'
@pytest.fixture
def sample_user():
return User(id=1, name='Alice', email='alice@example.com')
[他のスキルが一緒にデプロイされている場合、それぞれのスキルで完全な統合パターンが利用可能です]
その他のリソース
ライセンス: MIT(寛容ライセンスのため全文を引用しています) · 原本リポジトリ
詳細情報
- 作者
- bobmatnyc
- ライセンス
- MIT
- 最終更新
- 不明
Source: https://github.com/bobmatnyc/claude-mpm-skills / ライセンス: MIT
関連スキル
doubt-driven-development
重要な判断はすべて、本番環境への展開前に新しい視点から対抗的レビューを実施します。速度より正確性が重要な場合、不慣れなコードを扱う場合、本番環境・セキュリティに関わるロジック・取り消し不可の操作など影響度が高い場合、または後でバグを修正するよりも今検証する方が効率的な場合に活用してください。
apprun-skills
TypeScriptを使用したAppRunアプリケーションのMVU設計に関する総合的なガイダンスが得られます。コンポーネントパターン、イベントハンドリング、状態管理(非同期ジェネレータを含む)、パラメータと保護機能を備えたルーティング・ナビゲーション、vistestを使用したテストに対応しています。AppRunコンポーネントの設計・レビュー、ルートの配線、状態フローの管理、AppRunテストの作成時に活用してください。
desloppify
コードベースのヘルスチェックと技術負債の追跡ツールです。コード品質、技術負債、デッドコード、大規模ファイル、ゴッドクラス、重複関数、コードスメル、命名規則の問題、インポートサイクル、結合度の問題についてユーザーが質問した場合に使用してください。また、ヘルススコアの確認、次の改善項目の提案、クリーンアップ計画の作成をリクエストされた際にも対応します。29言語に対応しています。
debugging-and-error-recovery
テストが失敗したり、ビルドが壊れたり、動作が期待と異なったり、予期しないエラーが発生したりした場合に、体系的な根本原因デバッグをガイドします。推測ではなく、根本原因を見つけて修正するための体系的なアプローチが必要な場合に使用してください。
test-driven-development
テスト駆動開発により実装を進めます。ロジックの実装、バグの修正、動作の変更など、あらゆる場面で活用できます。コードが正常に動作することを証明する必要がある場合、バグ報告を受けた場合、既存機能を修正する予定がある場合に使用してください。
incremental-implementation
変更を段階的に実施します。複数のファイルに影響する機能や変更を実装する場合に使用してください。大量のコードを一度に書こうとしている場合や、タスクが一度では完結できないほど大きい場合に活用します。