Pydantic v2への移行で破壊的変更に対応する実例ガイド|エラー解決とコード変更方法

プログラミング

Pydanticのメジャーアップグレードに悩んでいませんか?v1からv2への移行で、それまで動いていたコードが突然エラーになる。設定が変わって何をどう修正すればいいのか分からない。そんな状況に陥っているエンジニアは少なくありません。

Pydantic v2は大幅な性能向上とAPI改善をもたらした一方で、互換性を保たない破壊的変更が数多く含まれています。しかし正しく理解して対応すれば、v2への移行は決して難しくありません。

本記事では、実際のエラーケースと具体的な解決策を通じて、Pydantic v2への移行をスムーズに進めるための実例ガイドを提供します。

Pydantic v2における主要な破壊的変更

Pydantic v2では、v1と互換性がない変更が複数あります。まずはどのような変更があるのかを理解することが重要です。

Configクラスからmodel_configへの移行

v1では、モデルの設定をConfigクラスで定義していました。v2ではこのアプローチが廃止され、model_configという新しい方法に統一されています。

v1の書き方:

from pydantic import BaseModel

class User(BaseModel):
    name: str
    age: int

    class Config:
        validate_assignment = True
        str_strip_whitespace = True

v2の書き方:

from pydantic import BaseModel, ConfigDict

class User(BaseModel):
    model_config = ConfigDict(
        validate_assignment=True,
        str_strip_whitespace=True
    )
    name: str
    age: int

この変更は単なる記法の違いではなく、Pydantic v2の設計思想全体に関わるものです。設定をより明示的かつ型安全に管理できるようになりました。

バリデーター関数の装飾子の変更

v1の@validatorデコレータはv2で廃止され、@field_validator@model_validatorに分かれました。

v1の書き方:

from pydantic import BaseModel, validator

class Product(BaseModel):
    price: float

    @validator('price')
    def price_must_be_positive(cls, v):
        if v < 0:
            raise ValueError('価格は正の数である必要があります')
        return v

v2の書き方(フィールドレベル):

from pydantic import BaseModel, field_validator

class Product(BaseModel):
    price: float

    @field_validator('price')
    @classmethod
    def price_must_be_positive(cls, v):
        if v < 0:
            raise ValueError('価格は正の数である必要があります')
        return v

v2の書き方(モデルレベル):

from pydantic import BaseModel, model_validator

class Product(BaseModel):
    price: float
    quantity: int

    @model_validator(mode='after')
    def check_inventory(self):
        if self.price * self.quantity > 1000000:
            raise ValueError('在庫価値が大きすぎます')
        return self

重要なポイントは、v2では@classmethodデコレータが明示的に必要になった点です。またmodeパラメータで検証のタイミングを制御できるようになりました。

シリアライゼーションと出力の変更

データの出力方法もPydantic v2で大きく変わりました。

dict()からmodel_dump()

v1の書き方:

user = User(name='太郎', age=30)
user_dict = user.dict()  # または user.dict(exclude={'age'})
user_json = user.json()

v2の書き方:

user = User(name='太郎', age=30)
user_dict = user.model_dump()  # または model_dump(exclude={'age'})
user_json = user.model_dump_json()

dict()json()メソッドはv2では完全に削除されています。既存コードで.dict()を使用している箇所は全て.model_dump()に置き換える必要があります。

JSONスキーマの生成方法の変更

v1の書き方:

schema = User.schema()
with open('schema.json', 'w') as f:
    json.dump(schema, f)

v2の書き方:

schema = User.model_json_schema()
with open('schema.json', 'w') as f:
    json.dump(schema, f)

実践的な移行手順

では実際に、既存のPydantic v1プロジェクトをv2に移行する手順を説明します。

ステップ1:依存関係の更新

pip install --upgrade pydantic

Pydantic v2は内部エンジンとしてRust製のpydantic-coreを使用しており、バリデーション速度が飛躍的に向上しています。

ステップ2:モデル定義の修正

以下は実際の移行例です。ブログAPIのレスポンスモデルを想定しています。

修正前(v1コード):

from pydantic import BaseModel, validator
from datetime import datetime
from typing import Optional

class Article(BaseModel):
    id: int
    title: str
    content: str
    author: str
    published_at: Optional[datetime] = None
    views: int = 0

    @validator('title')
    def title_not_empty(cls, v):
        if not v or len(v.strip()) == 0:
            raise ValueError('タイトルは空にできません')
        return v.strip()

    @validator('views')
    def views_must_positive(cls, v):
        if v < 0:
            raise ValueError('ビュー数は0以上である必要があります')
        return v

    class Config:
        validate_assignment = True
        json_encoders = {
            datetime: lambda v: v.isoformat()
        }

修正後(v2コード):

from pydantic import BaseModel, field_validator, ConfigDict
from datetime import datetime
from typing import Optional

class Article(BaseModel):
    model_config = ConfigDict(validate_assignment=True)

    id: int
    title: str
    content: str
    author: str
    published_at: Optional[datetime] = None
    views: int = 0

    @field_validator('title')
    @classmethod
    def title_not_empty(cls, v):
        if not v or len(v.strip()) == 0:
            raise ValueError('タイトルは空にできません')
        return v.strip()

    @field_validator('views')
    @classmethod
    def views_must_positive(cls, v):
        if v < 0:
            raise ValueError('ビュー数は0以上である必要があります')
        return v

v2ではJSON encodersが廃止され、シリアライゼーションはより柔軟なfield_serializerで対応します。

ステップ3:メソッド呼び出しの更新

既存コードの以下の呼び出しを修正します。

v1 v2
model.dict() model.model_dump()
model.json() model.model_dump_json()
model.json_schema() model.model_json_schema()
Model.parse_obj() Model.model_validate()
Model.parse_raw() Model.model_validate_json()
Model.construct() Model.model_construct()

よくあるエラーと解決策

Pydantic v2への移行で直面しやすいエラーの解決策をまとめました。

「ValidationError: Input should be a valid integer」エラー

このエラーはv2のバリデーションが厳格化されたことが原因です。文字列の「123」を整数として入力した場合、v1では自動的に変換されましたが、v2ではデフォルトで変換されません。

解決方法:

from pydantic import BaseModel, ConfigDict

class Product(BaseModel):
    model_config = ConfigDict(
        str_to_number=True  # v2では別途設定が必要
    )
    product_id: int

「AttributeError: 'User' object has no attribute 'Config'」

v1スタイルのConfigクラスを参照しているコードが残っている場合のエラーです。

解決方法:全てのモデル定義でclass Configmodel_config = ConfigDict(...)に置き換えてください。

カスタムバリデーターが動作しない問題

v2ではmode='before'mode='after'mode='wrap'の3つの実行タイミングがあります。デフォルトは'after'ですが、必要に応じて指定してください。

from pydantic import field_validator

class User(BaseModel):
    age: int

    @field_validator('age', mode='before')
    @classmethod
    def convert_age(cls, v):
        # 入力値がまだパース前の段階で実行
        if isinstance(v, str):
            return int(v)
        return v

AIを活

🤖 このブログはAIで自動運営しています。 同じ仕組みを御社にも導入できます。 無料相談はこちら
タイトルとURLをコピーしました