mcpField
P
pydantic

pydantic

github.com/midudev開発7.0k66 回インストール1 日前に更新
リポジトリ

pydantic について

Python 開発者向けの Pydantic v2 検証ガイドです。型付きモデル、解析、シリアライズ、設定管理の例に加え、一般的な連携や移行の指針を扱います。

主な機能

  1. 01型ヒントによる実行時検証
  2. 02フィールド制約と独自バリデーター
  3. 03モデルと JSON のシリアライズ
  4. 04環境変数やファイルからの設定読み込み
  5. 05FastAPI と ORM の連携
  6. 06v1 から v2 への移行ガイド

活用シーン

  1. 01API リクエストとレスポンスの検証
  2. 02JSON や辞書の解析とシリアライズ
  3. 03アプリケーション設定の読み込み
  4. 04ORM データ向けスキーマの設計

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/midudev/autoskills --skill pydantic

インストール後、Claude Code で/pydanticを入力して読み込まれたか確認します。

同梱ファイルmetadata.json

詳細

Pydantic バリデーションスキル

概要

Pydantic v2 の Rust 製コアを活用し、型ヒントと実行時の型チェックによって Python データを高性能に検証します。

使用する場面

  • API リクエスト/レスポンスの検証(FastAPI、Django)
  • 設定と構成の管理(環境変数、設定ファイル)
  • ORM モデルの検証(SQLAlchemy との統合)
  • データの解析とシリアライズ(JSON、dict、カスタム形式)
  • 自動検証機能付きの型安全なデータクラス
  • 型安全な 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

# Validate data
user = User(id=1, name="Alice", email="[email protected]")
print(user.model_dump())  # {'id': 1, 'name': 'Alice', ...}

# Automatic type coercion
user2 = User(id="2", name="Bob", email="[email protected]")
assert user2.id == 2  # String "2" coerced to int

# Validation error
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

# Usage
product = Product(name="  Widget  ", price=19.99)
assert product.name == "Widget"  # Whitespace stripped

# Validate on assignment
product.price = "29.99"  # Auto-converts to float
フィールドの設定
from pydantic import Field, field_validator
from typing import Annotated

class Item(BaseModel):
    # Field constraints
    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 types (Pydantic v2)
    quantity: Annotated[int, Field(ge=1, le=100)]

    # Descriptions and examples
    description: str = Field(
        ...,
        description="Product description",
        examples=["High-quality widget"]
    )

    # Deprecated fields
    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 replaced by serializers
    )

    @model_serializer
    def ser_model(self) -> dict:
        return {...}

# Key changes:
# - .dict() → .model_dump()
# - .json() → .model_dump_json()
# - .parse_obj() → .model_validate()
# - .parse_raw() → .model_validate_json()
# - @validator → @field_validator
# - @root_validator → @model_validator
パフォーマンスの改善
# v2 uses Rust core (pydantic-core) for 5-50x speedup
from pydantic import BaseModel
import time

class Data(BaseModel):
    values: list[int]
    names: list[str]

# Benchmark
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 validation
    email: EmailStr

    # URL validation
    website: HttpUrl

    # UUID
    id: UUID4

    # File system paths
    config_file: FilePath
    data_dir: DirectoryPath

    # JSON string → parsed object
    metadata: Json[dict[str, str]]

    # Secret (won't print in logs)
    api_key: SecretStr

    # Constrained types
    age: PositiveInt
    balance: NegativeFloat
    username: constr(min_length=3, max_length=20, pattern=r'^[a-z]+$')
    code: conint(ge=1000, le=9999)

    # Literal types
    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

# Usage
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

    # Validate multiple fields
    @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:
        # Pre-processing before validation
        if isinstance(data, dict) and 'total' not in data:
            data['total'] = len(data.get('items', [])) * 10.0
        return data
ルートバリデーター(ラップ)
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):
        # Call default validation
        result = handler(values)

        # Post-validation logic
        if result.env == 'prod' and result.debug:
            raise ValueError('debug cannot be True in production')

        return result

型変換と厳密モード

from pydantic import BaseModel, ConfigDict, ValidationError

# Coercive mode (default)
class CoerciveModel(BaseModel):
    count: int
    price: float

data = CoerciveModel(count="42", price="19.99")
assert data.count == 42  # String → int
assert data.price == 19.99  # String → float

# Strict mode
class StrictModel(BaseModel):
    model_config = ConfigDict(strict=True)

    count: int
    price: float

try:
    StrictModel(count="42", price="19.99")  # Raises ValidationError
except ValidationError as e:
    print("Strict mode: no coercion allowed")

# Per-field strict mode
class MixedModel(BaseModel):
    flexible: int  # Allows coercion
    strict: Annotated[int, Field(strict=True)]  # No coercion

MixedModel(flexible="1", strict=2)  # OK
# MixedModel(flexible="1", strict="2")  # ValidationError

ネストされたモデルと再帰型

from pydantic import BaseModel
from typing import ForwardRef

# Nested models
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'}
)

# Recursive types (tree structure)
class TreeNode(BaseModel):
    value: int
    children: list['TreeNode'] = []

TreeNode.model_rebuild()  # Required for forward references

tree = TreeNode(
    value=1,
    children=[
        TreeNode(value=2, children=[TreeNode(value=4)]),
        TreeNode(value=3)
    ]
)

# Self-referencing with 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

# Usage with concrete type
user_response = Response[User](
    success=True,
    data=User(id=1, name='Alice')
)

# List response
list_response = Response[list[User]](
    success=True,
    data=[User(id=1, name='Alice'), User(id=2, name='Bob')]
)

# Generic repository pattern
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'))

シリアライズ

モデルのダンプ
from pydantic import BaseModel, Field, field_serializer

class Article(BaseModel):
    title: str
    content: str
    tags: list[str]
    metadata: dict[str, Any] = {}

    # Serialization customization
    @field_serializer('tags')
    def serialize_tags(self, tags: list[str]) -> str:
        return ','.join(tags)

article = Article(
    title='Pydantic Guide',
    content='...',
    tags=['python', 'validation']
)

# Dump to dict
data = article.model_dump()
# {'title': 'Pydantic Guide', 'tags': 'python,validation', ...}

# Exclude fields
data = article.model_dump(exclude={'metadata'})

# Include only specific fields
data = article.model_dump(include={'title', 'tags'})

# Exclude unset fields
article2 = Article(title='Test', content='...', tags=[])
data = article2.model_dump(exclude_unset=True)  # metadata excluded

# By alias
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())

# Dump to JSON string
json_str = event.model_dump_json()
# '{"name":"Deploy","timestamp":"2025-11-30T..."}'

# Pretty print
json_str = event.model_dump_json(indent=2)

# Parse from 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,
            # Never serialize password
        }

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
    )

    # Environment variables
    database_url: str
    redis_url: str = 'redis://localhost:6379'
    secret_key: SecretStr
    debug: bool = False

    # Nested settings
    class SMTPSettings(BaseModel):
        host: str
        port: int = 587
        username: str
        password: SecretStr

    smtp: SMTPSettings

# Reads from environment variables:
# APP_DATABASE_URL, APP_REDIS_URL, APP_SECRET_KEY, APP_DEBUG
# APP_SMTP__HOST, APP_SMTP__PORT, etc.

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()

# Usage in 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 auto-validates request body
    # Returns only fields in UserResponse (password excluded)
    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='[email protected]',
        created_at=datetime.now(),
        last_login=None
    )

    if not include_dates:
        return user.model_dump(exclude={'created_at', 'last_login'})
    return user

SQLAlchemy との統合

Pydantic を使用した ORM モデル
from sqlalchemy import Column, Integer, String, DateTime
from sqlalchemy.orm import DeclarativeBase
from pydantic import BaseModel, ConfigDict

class Base(DeclarativeBase):
    pass

# SQLAlchemy ORM model
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 model for validation
class UserSchema(BaseModel):
    model_config = ConfigDict(from_attributes=True)

    id: int
    username: str
    email: EmailStr
    created_at: datetime

# Usage
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 operations
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 model
class Article(models.Model):
    title = models.CharField(max_length=200)
    content = models.TextField()
    published = models.BooleanField(default=False)

# Pydantic schema
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

# Usage in Django views
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

# Computed fields in serialization
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

# Custom error handling
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 uses pydantic-core (Rust) for:
# - 5-50x faster validation
# - Lower memory usage
# - Better error messages
# - Improved JSON parsing

import timeit
from pydantic import BaseModel

class Data(BaseModel):
    values: list[int]
    names: list[str]
    metadata: dict[str, Any]

# Benchmark
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 only when needed
        validate_assignment=False,

        # Disable validation for internal use
        validate_default=False,

        # Use slots for memory efficiency
        # (Not available in Pydantic v2 BaseModel directly)
    )

    data: list[int]

# Reuse validators
from functools import lru_cache

@lru_cache(maxsize=128)
def get_validator(model_class):
    return model_class.model_validate

# Bulk validation
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")

# Generate 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 compatible
from fastapi import FastAPI

app = FastAPI()

@app.post('/products')
def create_product(product: Product):
    return product

# FastAPI auto-generates OpenAPI schema from Pydantic models

データクラスとの統合

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'.+@.+\..+')

# Works like Pydantic BaseModel with validation
user = User(id=1, name='Alice', email='[email protected]')

# Validation on construction
try:
    User(id=2, name='', email='invalid')
except ValidationError as e:
    print(e.errors())

# Convert to 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():
    # Valid data
    user = User(id=1, name='Alice', email='[email protected]')
    assert user.name == 'Alice'

    # Invalid data
    with pytest.raises(ValidationError) as exc_info:
        User(id='invalid', name='Bob', email='[email protected]')

    errors = exc_info.value.errors()
    assert errors[0]['type'] == 'int_parsing'

def test_user_serialization():
    user = User(id=1, name='Alice', email='[email protected]')
    data = user.model_dump()

    assert data == {
        'id': 1,
        'name': 'Alice',
        'email': '[email protected]'
    }

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': '[email protected]'
    }

@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

    # Validators
    @validator('field')
    def validate_field(cls, v):
        return v

    @root_validator
    def validate_model(cls, values):
        return values

    # Serialization
    data = model.dict()
    json_str = model.json()

    # Parsing
    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 validators
    @field_validator('field')
    @classmethod
    def validate_field(cls, v):
        return v

    # Model validators
    @model_validator(mode='after')
    def validate_model(self):
        return self

    # Serialization
    data = model.model_dump()
    json_str = model.model_dump_json()

    # Parsing
    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 を @classmethod と併用する @field_validator に更新する
  • @root_validator を @model_validator(mode='after') に更新する
  • json_encoders を見直し、@field_serializer を使用する
  • 厳密モードの動作変更をテストする
  • カスタム型を __get_pydantic_core_schema__ を使用するよう更新する

ベストプラクティス

モデルの構成
# Separate schemas by use case
class UserBase(BaseModel):
    """Shared fields"""
    username: str
    email: EmailStr

class UserCreate(UserBase):
    """API request for creating user"""
    password: str

class UserUpdate(BaseModel):
    """API request for updating user (all optional)"""
    username: str | None = None
    email: EmailStr | None = None
    password: str | None = None

class UserInDB(UserBase):
    """Database representation"""
    model_config = ConfigDict(from_attributes=True)

    id: int
    password_hash: str
    created_at: datetime

class UserResponse(UserBase):
    """API response (excludes sensitive data)"""
    id: int
    created_at: datetime
バリデーションのベストプラクティス
# Use Field for constraints, not validators
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

# Prefer composition over inheritance
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:
        # Log validation errors
        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 user-friendly errors
        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] = {}

# Usage
user_response = APIResponse[User](
    success=True,
    data=User(id=1, name='Alice', email='[email protected]')
)

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 with Pydantic (basic pattern)
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 auto-validates using Pydantic
    # response_model filters out password
    return UserResponse(id=1, username=user.username, email=user.email)
SQLAlchemy とのクイック統合リファレンス(単独利用向けにインライン化)
# SQLAlchemy 2.0 with Pydantic validation
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

# Convert ORM to Pydantic
user_orm = db.query(UserDB).first()
user_validated = UserSchema.model_validate(user_orm)
Pytest テストのクイックリファレンス(単独利用向けにインライン化)
# Testing Pydantic models with pytest
import pytest
from pydantic import ValidationError

def test_user_validation():
    user = User(id=1, name='Alice', email='[email protected]')
    assert user.name == 'Alice'

def test_validation_error():
    with pytest.raises(ValidationError) as exc_info:
        User(id='invalid', name='Bob', email='[email protected]')
    errors = exc_info.value.errors()
    assert errors[0]['type'] == 'int_parsing'

@pytest.fixture
def sample_user():
    return User(id=1, name='Alice', email='[email protected]')

[すべての統合パターンは、各スキルを併せてデプロイした場合に利用できます]

追加リソース

コメント