Python

Pydantic v2のConfigDict設定一覧|model_configの書き方とv1からの移行

Pydantic v2のConfigDict設定一覧|model_configの書き方とv1からの移行

Pydantic v2でモデルの挙動を変えたいときの入口は、model_config に ConfigDict を代入する1行です。ただし「どのオプションの既定値が何で、違反したときにどんなエラーが返るのか」は公式リファレンスに散っていて、v1の class Config 世代の解説と混ざったまま検索結果に残っています。この記事では、Pydantic 2.13.5を実際に動かして得た出力を根拠に、ConfigDictの主要オプションとv1からの移行手順を整理します。

まとめ:ConfigDictで押さえる要点

以下に要点を示します。コード例は本文順に同じPythonセッションで実行し、前の例のimportやモデル定義を引き継ぎます。

  • 本記事では model_config = ConfigDict(...) を使う。v2では通常の辞書やクラス定義のキーワード引数による設定も可能。v1の class Config も動くが、実行すると「Deprecated in Pydantic V2.0 to be removed in V3.0」という警告が出る。
  • 主要オプションの既定値は extra='ignore'、strict=False、frozen=False、validate_assignment=False、validate_by_alias=True、validate_by_name=False。設定していないモデルの model_config は空辞書のまま。
  • populate_by_name は公式ドキュメントが「v2.11以降は推奨せず、v3で非推奨化する」と明記。置き換え先は validate_by_name=True と validate_by_alias=True の2つ。
  • 検証は model_validate、出力は model_dump / model_dump_json。model_construct は検証を丸ごと飛ばすため、信頼できる入力にしか使えない。
  • 移行ツールのbump-pydanticは0.8.0(2023年12月28日)を最後に更新が止まり、リポジトリもアーカイブ済み。利用する場合は自動変換の差分を確認し、非推奨警告とテスト結果を併用して残りを修正する。

Pydanticそのものの位置づけや読み方から確認したい場合は、Pydanticとは?Pythonデータ検証ライブラリの使い方と基本【V2対応】で基礎を押さえてから戻ってくると読みやすくなります。

model_configとConfigDictの基本形

class Configが出す非推奨警告と削除予定

v1では設定を内部クラス Config に書きました。v2でも動作しますが、クラス定義の時点で PydanticDeprecatedSince20 が発生します。2.13.5で実行すると、次のメッセージが逐語で返ります。

import warnings
from pydantic import BaseModel

with warnings.catch_warnings(record=True) as w:
    warnings.simplefilter("always")

    class M(BaseModel):
        class Config:
            extra = "forbid"

        a: int

    for x in w:
        print(type(x.message).__name__, "|", x.message)

print("model_config ->", M.model_config)

# PydanticDeprecatedSince20 | Support for class-based `config` is deprecated, use ConfigDict
#   instead. Deprecated in Pydantic V2.0 to be removed in V3.0. See Pydantic V2 Migration Guide
#   at https://errors.pydantic.dev/2.13/migration/
# model_config -> {'extra': 'forbid'}

注目したいのは2行目です。class Config の中身は内部で model_config の辞書に変換されており、挙動そのものは同じです。この例のextraのように、名前が変わっていない設定は書式の置換だけで移せます。ただし orm_mode のように改名された項目や削除された項目があるため、設定ごとの確認は要ります。class Config という書式そのものが消えるのはV3ですが、個別設定の非互換はv2へ上げた時点で表面化します。なお、移行の途中で両方を同じモデルに書くことはできません。class Config と model_config を併記すると PydanticUserError になり、"Config" and "model_config" cannot be used together というメッセージでクラス定義そのものが失敗します。クラス単位で一度に切り替えてください。

ConfigDictの書き方と継承時の上書き

v2の書式はクラス属性への代入1行です。基底クラスで設定した内容は派生クラスに引き継がれ、同じキーを指定した場合だけ上書きされます。

from pydantic import BaseModel, ConfigDict

class Base(BaseModel):
    model_config = ConfigDict(extra="forbid", str_strip_whitespace=True)

class Child(Base):
    model_config = ConfigDict(extra="allow")  # extraだけ上書き

print(Child.model_config)
# {'extra': 'allow', 'str_strip_whitespace': True}

ConfigDict は実体が TypedDict なので、キー名の打ち間違いを型チェッカが指摘できます。ただし実行時のチェックはありません。2.13.5で ConfigDict(extra_typo="forbid") と書いても例外は出ず、そのキーは効かないまま model_config に残りました。設定が反映されない不具合を静的に拾えるようになる点が、class Config から書き換える実利のひとつです。

ConfigDict主要オプションの既定値と違反時のエラー

検索されやすい6つのオプションについて、既定値と、条件に反したときに ValidationError.errors() が返すエラー種別を実測値でまとめます。エラー種別(type)はメッセージ文字列と違って安定しているので、例外処理の分岐に使えます。

オプション 既定値 役割 違反時のエラー種別
extra 'ignore' 未定義キーの扱い extra_forbidden
strict False 型変換の可否 int_type ほか
frozen False 代入禁止とハッシュ生成 frozen_instance
validate_assignment False 代入時の再検証 int_parsing ほか
validate_by_alias True エイリアス名での投入 missing
validate_by_name False フィールド名での投入 missing

この表は2.13.5の実行結果と公式のConfiguration リファレンスを突き合わせたものです。以下、実務で判断が分かれる4点を個別に見ます。

extraの3つの挙動(ignore・forbid・allow)

既定の 'ignore' は、モデルに無いキーを黙って捨てます。APIのレスポンスをそのまま流し込むと、typoしたキーが誰にも気づかれないまま消えるのはこの動作のせいです。'forbid' にすると即座に例外になります。

from pydantic import BaseModel, ConfigDict, Field, ValidationError

class U(BaseModel):
    model_config = ConfigDict(extra="forbid")
    name: str

try:
    U(name="a", age=3)
except ValidationError as e:
    print(e.errors())

# [{'type': 'extra_forbidden', 'loc': ('age',), 'msg': 'Extra inputs are not permitted',
#   'input': 3, 'url': '...v/extra_forbidden'}]

'allow' は未定義キーを捨てずに保持し、model_extra から取り出せます。model_dump() の結果にも含まれます。

class E(BaseModel):
    model_config = ConfigDict(extra="allow")
    a: int

e = E(a=1, b="x")
print(e.model_extra)   # {'b': 'x'}
print(e.model_dump())  # {'a': 1, 'b': 'x'}

外部から受け取る設定ファイルやWebhookのペイロードは 'forbid' を既定にしておくべきです。仕様変更でキーが増えたときに例外で気づけます。逆に、上流の追加項目を落とさず通す中継処理では 'allow' が要ります。既定の 'ignore' が妥当なのは、上流が項目を増やすことを前提に必要な項目だけ取り出す用途です。契約外の項目を拒否するなら 'forbid'、保持するなら 'allow'、捨ててよいと判断したうえで 'ignore' を選ぶ、と決めておくと迷いません。

strictモードとlaxモードの型変換の差

既定のlaxモードは、文字列の "5" を int の 5 に変換します。strict=True にするとこの変換が止まります。

class L(BaseModel):
    n: int

class S(BaseModel):
    model_config = ConfigDict(strict=True)
    n: int

print(L(n="5").n, type(L(n="5").n))  # 5 <class 'int'>

try:
    S(n="5")
except ValidationError as e:
    print(e.errors()[0]["type"], "|", e.errors()[0]["msg"])
# int_type | Input should be a valid integer

strictをモデル全体に掛けると、フォームやクエリ文字列から届く文字列をintなどへ変換するフィールドで検証エラーになります。strフィールドまで一律に失敗するわけではありません。全体で有効化するより、金額や個数など変換されると困るフィールドだけ Field(strict=True) で個別指定するほうが破綻しにくい設計です。

frozenとvalidate_assignmentの使い分け

この2つは「生成後の書き換え」に関する設定で、狙いが逆です。frozen=True は代入そのものを禁止し、同時に __hash__ を生成します。ただし辞書のキーや集合の要素にできるのは、すべての属性値がハッシュ可能な場合です。validate_assignment=True は代入を許したうえで、そのたびに検証を走らせます。

class F(BaseModel):
    model_config = ConfigDict(frozen=True)
    x: int

try:
    F(x=1).x = 2
except ValidationError as e:
    print(e.errors()[0]["type"])  # frozen_instance

class VA(BaseModel):
    model_config = ConfigDict(validate_assignment=True)
    x: int

va = VA(x=1)
try:
    va.x = "abc"
except ValidationError as e:
    print(e.errors()[0]["type"])  # int_parsing

class NoVA(BaseModel):
    x: int

n = NoVA(x=1)
n.x = "abc"
print(repr(n.x))  # 'abc'

最後の3行が重要です。既定では代入時に検証は走りません。生成時の検証を通っても、その後の代入で型に合わない値が入ることがあります。なお、frozenやvalidate_assignmentでも、属性内のリストへのappendや辞書の要素更新までは防げません。設定オブジェクトやドメインモデルのように生成後に変わらない値は frozen=True、状態を持つモデルは validate_assignment=True と決めておくと、この穴を塞げます。

populate_by_nameからvalidate_by_name・validate_by_aliasへの移行

エイリアスを付けたフィールドをPython側の名前でも埋めたいとき、長らく populate_by_name=True が使われてきました。公式のConfiguration リファレンスは現在このオプションについて「v2.11以降での使用は推奨されず、v3で非推奨になる」と注意書きを置き、validate_by_name=True と validate_by_alias=True の併用を代替として案内しています。

class A(BaseModel):
    model_config = ConfigDict(populate_by_name=True)
    user_id: int = Field(alias="userId")

print(A.model_config)
# {'populate_by_name': True, 'validate_by_alias': True, 'validate_by_name': True}

2.13.5で確認した限り、populate_by_name=True はまだ警告を出さず、内部で新しい2つのフラグの両方をTrueに解決していました。つまり今すぐ壊れる変更ではありません。ただし2つに分かれたことで、旧オプションでは表現できなかった設定が書けます。

class C(BaseModel):
    model_config = ConfigDict(validate_by_name=True, validate_by_alias=False)
    user_id: int = Field(alias="userId")

print(C(user_id=3))       # user_id=3

try:
    C(userId=3)
except ValidationError as e:
    print(e.errors()[0]["type"], e.errors()[0]["loc"])  # missing ('user_id',)

エイリアスは出力(model_dump(by_alias=True))専用にして、入力はPython側の名前だけに絞る、という設計がこれで表現できます。新規に書くコードは最初から validate_by_name と validate_by_alias の2つを明示すると、現在の公式推奨に沿った設定になります。なお両方をFalseにすると値を入れる手段が無くなるため、その組み合わせは拒否されます。

入力の検証とValidationErrorの扱い

model_validateとコンストラクタ、model_validate_jsonの使い分け

辞書を渡すなら model_validate、JSON文字列をそのまま渡すなら model_validate_json です。v1の parse_obj と parse_raw が非推奨になった置き換え先にあたります。

class P(BaseModel):
    user_id: int = Field(alias="userId")

print(P.model_validate({"userId": 8}))          # user_id=8
print(P.model_validate_json('{"userId": 9}'))   # user_id=9

コンストラクタ P(userId=8) との違いは、入力形式に加え、検証時のオプション指定にもあります。model_validateではstrictやcontextなどを呼び出し時に指定できますが、通常のコンストラクタでは同じ指定はできません。外部から届いたJSONは model_validate_json に渡すほうが、辞書へ一度展開する分の処理を省けます。後述の実測環境で同じ注文モデルを2万回処理したところ、json.loads を挟んでから model_validate に渡す経路が77.0us、model_validate_json に直接渡す経路が48.0usでした。

ValidationError.errors()の構造とエラー種別での分岐

ValidationError は例外メッセージを文字列で読むのではなく、errors() の戻り値で扱います。返るのは辞書のリストです。エラーによっては追加情報のctxが含まれ、カスタムエラーではurlがない場合もあります。主なキーは type、loc、msg、input、url です。

try:
    U(name="a", age=3)
except ValidationError as e:
    for er in e.errors():
        print(er["loc"], er["type"], er["msg"])
# ('age',) extra_forbidden Extra inputs are not permitted

loc はネストした位置をタプルで返すため、('items', 0, 'id') のように配列の何番目のどのフィールドで落ちたかまで特定できます。APIのエラーレスポンスを組み立てるときは、msg をそのまま出さず type で分岐して自前の日本語文言に置き換えると、Pydanticのバージョン差でメッセージが変わっても壊れません。

model_constructで検証を飛ばしてよい場面

model_construct はバリデーションを一切実行せずにインスタンスを組み立てます。型が合っていない値もそのまま通ります。

bad = P.model_construct(user_id="not-an-int")
print(repr(bad.user_id), type(bad.user_id))  # 'not-an-int' <class 'str'>

使ってよいのは、モデルの型と制約を満たすと保証できるデータを扱う場合です。DB由来でも、列の型や制約がモデルと一致する保証がなければ検証を省略できません。外部入力に対して速度目的で使うと、型の保証が無いオブジェクトが下流に流れます。速さが要るなら、検証を捨てるのではなく model_validate_json でJSONを直接渡す経路に変えるほうが安全です。

model_dumpとmodel_dump_jsonによる出力制御

出力側はv1の .dict() と .json() が非推奨になり、model_dump と model_dump_json に統一されました。両者の差はJSONに直列化するかどうかで、辞書のままJSON互換の値がほしいときは mode="json" を使います。

import datetime

class D(BaseModel):
    at: datetime.datetime

d = D(at=datetime.datetime(2026, 9, 10, 12, 0))
print(d.model_dump())                # {'at': datetime.datetime(2026, 9, 10, 12, 0)}
print(d.model_dump(mode="json"))     # {'at': '2026-09-10T12:00:00'}

p = P.model_validate({"userId": 7})
print(p.model_dump())                # {'user_id': 7}
print(p.model_dump(by_alias=True))   # {'userId': 7}

mode="json" を挟まずに model_dump() の結果を json.dumps に渡すと、datetime や UUID で例外になります。テンプレートに渡すだけなら既定のPythonオブジェクトのまま、外部へ送るなら mode="json" か model_dump_json、と経路で決めておくと迷いません。除外は exclude と include、未設定値の省略は exclude_unset=True で指定します。

フィールド制約とバリデータの書き方

conint・constrからAnnotatedへの移行と制約の比較

conint や constr といった制約付き型は2.13.5でも警告なく動きますが、公式リファレンスは「使用は推奨されない(discouraged)」と位置づけ、Pydantic 3.0で非推奨化する予定だと明記しています。理由も書かれていて、これらの関数が型を返すため静的解析ツールと相性が悪い、というものです。公式の置換推奨はconintではAnnotatedとField、constrではAnnotatedとStringConstraintsです。以下の数値範囲と文字列長の制約はFieldでも指定でき、生成されるJSONスキーマが一致することは実行して確認できます。

from typing_extensions import Annotated
from pydantic import conint, constr

class Old(BaseModel):
    age: conint(ge=0, le=120)
    code: constr(min_length=2, max_length=4)

class New(BaseModel):
    age: Annotated[int, Field(ge=0, le=120)]
    code: Annotated[str, Field(min_length=2, max_length=4)]

print(Old.model_json_schema()["properties"]["age"]
      == New.model_json_schema()["properties"]["age"])  # True
print(New.model_json_schema()["properties"]["age"])
# {'maximum': 120, 'minimum': 0, 'title': 'Age', 'type': 'integer'}

Annotated 側を選ぶ理由は、型そのものは素の int や str のままなので型チェッカや補完が正しく効き、制約だけを別名で使い回せる点にあります。Age = Annotated[int, Field(ge=0, le=120)] と定義しておけば複数モデルで共有できます。

field_validatorとmodel_validatorの役割分担

単一フィールドの検証は @field_validator、フィールドをまたぐ整合性チェックや導出値の設定は @model_validator です。v1の @validator と @root_validator の置き換え先にあたります。

from typing import Optional
from pydantic import field_validator, model_validator

class Order(BaseModel):
    qty: int
    unit_price: int
    total: Optional[int] = None

    @field_validator("qty")
    @classmethod
    def positive(cls, v: int) -> int:
        if v <= 0:
            raise ValueError("qty must be positive")
        return v

    @model_validator(mode="after")
    def fill_total(self):
        if self.total is None:
            self.total = self.qty * self.unit_price
        return self

print(Order(qty=3, unit_price=100))
# qty=3 unit_price=100 total=300

@field_validator には @classmethod を付けます。ただし、第1引数がclsなら省略してもクラスメソッドとして扱われます。明示するのは型チェッカに意図を伝えるためです。mode="after" の model_validator は検証済みのインスタンス自身を受け取るので、上の例のように self の属性へ代入して self を返す形で書きます。raise ValueError は自動的に ValidationError に包まれ、エラー種別は value_error、位置はfield_validatorならそのフィールド名になります。モデル全体のmodel_validatorで発生したエラーは、最上位モデルでは通常、空のタプルになります。

OptionalのNone許容とフィールド省略の条件

v1から移ってきた人がまず引っかかるのがこれです。v2では Optional[int] は「Noneを許容する」だけで、デフォルト値を書かない限り必須フィールドのままです。

class O(BaseModel):
    a: Optional[int]        # 必須。Noneは入れられるが省略はできない
    b: Optional[int] = None # 省略可能

try:
    O(b=1)
except ValidationError as e:
    print(e.errors()[0]["loc"], e.errors()[0]["type"])  # ('a',) missing

省略可能にしたいなら = None を明示します。可変オブジェクトを既定値にする場合、Pydanticはハッシュ不能な既定値をインスタンスごとに深くコピーするため、素の tags: list = [] と書いてもインスタンス間で共有されません。標準の dataclasses で禁じられている書き方が、ここでは安全に通ります。

class C(BaseModel):
    tags: list = []

c1, c2 = C(), C()
c1.tags.append("a")
print(c1.tags, c2.tags)  # ['a'] []

Field(default_factory=list) が要るのは、既定値の生成方法を明示したいときや、現在時刻やUUIDのようにインスタンスごとに評価し直したい値を既定値にするときです。共有事故を避けるためだけに付ける必要はありません。

v1からv2への移行で最初に直す箇所

旧APIの置換は移行の入口です。続いて、設定項目の変更、Optionalの必須性、型変換、バリデータの挙動をテストで確認します。対応関係は次のとおりです。

v1 v2
class Config model_config = ConfigDict(…)
.dict() .model_dump()
.json() .model_dump_json()
parse_obj() model_validate()
parse_raw()(JSON入力) model_validate_json()
@validator @field_validator
@root_validator @model_validator
orm_mode from_attributes
allow_population_by_field_name validate_by_name

移行ツールとpydantic.v1名前空間の現状

公式が案内する自動置換ツールのbump-pydanticは、PyPI上の最新が0.8.0で、公開日は2023年12月28日です。GitHubのリポジトリもアーカイブ済みで、新規のissueやPRを受け付けていません。ベータ扱いのまま更新が止まっている以上、これに全面依存する前提は立てられません。上の表の置換と、実行して出る非推奨警告の潰し込みを手順にしたほうが確実です。

段階移行の逃げ道として、v2パッケージには pydantic.v1 名前空間が同梱されています。2.13.5で確認すると1.10.26が入っており、旧APIをそのまま呼べます。

import pydantic, pydantic.v1 as v1

print(pydantic.VERSION, v1.VERSION)  # 2.13.5 1.10.26

公式の移行ガイドはこの名前空間について削除時期を示していません。ただし恒久的な置き場所と考えるべきではなく、v1のままのモジュールをここへ退避させて、順次v2記法へ移す一時的な足場として使うのが妥当です。

GenericModelからBaseModelとGenericへの移行

v1でジェネリックなモデルを書くときに継承した pydantic.generics.GenericModel は、v2では BaseModel への別名に置き換わり、importすると非推奨警告が出ます。

from typing import Generic, List, TypeVar

T = TypeVar("T")

class Page(BaseModel, Generic[T]):
    items: List[T]
    total: int

class User(BaseModel):
    id: int

print(Page[User].model_validate({"items": [{"id": 1}], "total": 1}))
# items=[User(id=1)] total=1

v2では BaseModel と typing.Generic を多重継承するだけで済みます。型引数を与えた Page[User] は要素の型まで検証し、失敗時の loc は ('items', 0, 'id') のように配列の添字を含みます。

V1とV2の処理速度:同一環境での実測比較

公式のV2リリース記事は「pydantic V2はpydantic V1.9.1より4倍から50倍高速で、一般的なフィールドを含むモデルの検証ではおよそ17倍速い」と書いています。ただし倍率はモデルの形と経路で大きく変わるため、この幅のどこに自分のコードが落ちるかは測らないと分かりません。2.13.5と、同梱のpydantic.v1 1.10.26を同一プロセスで計測しました。ひとつ断りを入れると、同梱のV1は pydantic compiled: False と自己申告するとおりCython未コンパイルのPython実装で、単体配布のコンパイル済みV1との比較ではありません。条件は次のとおりです。

  • 環境:macOS(x86_64)、Python 3.9.6、pydantic 2.13.5、pydantic-core 2.46.5、pydantic.v1 1.10.26
  • 対象:id・name・price・tags を持つ子モデルを10件ネストした注文モデル
  • 計測:timeit でN=20,000回、2回反復して再現を確認。表のusは、合計時間をNで割った1回当たりのマイクロ秒
処理 V2 V1 比
辞書の検証 41.0us 528.7us 12.9倍
JSON文字列の検証 50.3us 575.8us 11.4倍
JSON出力 27.3us 470.4us 17.2倍

ネストした子モデルを実際に検証させると10倍以上の差が出ました。一方、同じ計測で items を型引数なしの list に変え、子モデルのフィールドを検証させないようにすると、辞書の検証は4.3倍前後まで縮みます。差を生むのは検証させるフィールドの量であって、公式が示す4倍から50倍という幅も、この違いを反映したものと読めます。ただしフィールド数だけで倍率が決まるわけではないので、平坦なモデルを少数回検証する処理では、アプリケーション全体に占める検証時間そのものを測ってください。

ここからの判断ははっきりしています。速度だけを理由にv1からの移行を計画するなら、本番相当のモデルで検証時間と処理件数を先に測るべきです。上の数値は特定の条件下の測定値で、CPUとPythonのバージョンが変われば絶対値は動きます。測らずに移行を正当化できる理由は別にあって、class Config や旧メソッドがV3.0で削除される予定であること、そして ConfigDict の設定ミスを型チェッカで拾えることの2点です。速度は移行の結果であって、動機に据えるには測定が要ります。

FastAPIと組み合わせて使う場合の全体像はFastAPIとは|最短で動かす手順とasync/defの使い分け【2026年版】、DBのテーブル定義とモデルを1つにまとめたい場合はSQLModelとは?SQLAlchemyとPydanticを統合したPython製ORMの特徴と使い方が参考になります。

よくある質問

Pydantic v2でclass Configはまだ使えますか?

2.13.5でも動作し、内部で model_config の辞書へ変換されます。ただしクラス定義の時点で「Deprecated in Pydantic V2.0 to be removed in V3.0」という警告が出るため、V3.0で削除されます。動くうちに model_config = ConfigDict(...) へ置き換えてください。

model_config = ConfigDict(extra="forbid") は何をする設定ですか?

モデルに定義されていないキーが入力に含まれていたら ValidationError を発生させます。エラー種別は extra_forbidden、loc には余分だったキー名が入ります。既定は 'ignore' で、この場合は未定義キーが黙って捨てられます。

populate_by_name=True は今も使えますか?

2.13.5では警告なしに動き、内部で validate_by_name と validate_by_alias の両方をTrueに解決します。ただし公式ドキュメントは「v2.11以降は推奨せず、v3で非推奨になる」と明記しているため、新規のコードは validate_by_name=True と validate_by_alias=True を明示するほうが安全です。

model_validateとコンストラクタはどう違いますか?

通常は同じモデル定義で検証しますが、model_validateではstrictやcontextなどを呼び出し時に指定できます。入力の渡し方は、コンストラクタはキーワード引数、model_validate は辞書などのオブジェクトを1つ受け取ります。外部から届いたJSON文字列は、辞書に展開せず model_validate_json に直接渡すのが最短経路です。検証を飛ばす model_construct は別物で、型が合わない値もそのまま通します。

v1のコードを残したまま移行できますか?

できます。v2パッケージには pydantic.v1 名前空間が同梱されており、2.13.5では1.10.26が入っています。import pydantic.v1 as v1 のように読み替えれば旧APIがそのまま動くため、モジュール単位で順に移せます。ただし公式は削除時期を明示していないので、恒久的な置き場所とは考えないでください。

関連記事

お気に入りに入れた記事の一覧

資料請求

RELATED POSTS 関連記事

目次