20 人が閲覧(直近 30 日) Python

Pydantic v2のConfigDict設定一覧|model_configの書き方とv1からの移行【2.14対応】

Pydantic v2のConfigDict設定一覧|model_configの書き方とv1からの移行【2.14対応】

Pydantic v2でモデルの挙動を変えるときは、クラス属性 model_config に ConfigDict を代入します。ConfigDictは設定項目を型付きで並べた辞書(TypedDict)で、未定義のキーを拒否する、型変換を止める、インスタンスを変更不可にする、といった指定をモデル単位でまとめて行えます。この記事では2026年10月11日公開のPydantic 2.14.1をPython 3.12で実際に動かし、主要オプションの既定値と違反時のエラー、2.14で変わった点、v1の class Config からの移行手順を出力つきで整理します。

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

コード例は本文の順に1つのPythonファイルとして実行しており、前の例のimportやモデル定義を後の例が使います。

  • 書き方は model_config = ConfigDict(...)、通常の辞書、class M(BaseModel, extra="forbid") のクラス引数の3通りで、結果は同じ。model_config 側でキーのつづりを間違えても実行時は何も起きず、mypyなどの型チェッカだけが検出する(クラス引数で間違えた場合は定義時に TypeError になる)。
  • 主要オプションの既定値は extra='ignore'、strict=False、frozen=False、validate_assignment=False、from_attributes=False、validate_by_alias=True、validate_by_name=False、serialize_by_alias=False。
  • 設定はモデルごとに閉じている。親モデルに extra="forbid" を付けても、フィールドとして持つ子モデルの余分なキーは黙って捨てられる。
  • 2.14から model_config は書いたとおりの内容を保つ。populate_by_name=True を書いても、2.13までのように validate_by_name などが自動で書き足されない。
  • populate_by_name は公式が「v2.11以降は推奨せず、v3で非推奨にする」と明記。新規コードは validate_by_name=True と validate_by_alias=True で書く。
  • 2.14はPython 3.9を対象外にした。3.9の環境で pip install pydantic を実行すると、入るのは3.9に対応する2.13系まで。

Pydanticそのものの位置づけや読み方から確認したい場合は、Pydanticとは?読み方・BaseModelの使い方とV1との違い【2.13対応】で基礎を押さえてから戻ってくると読みやすくなります。

model_configとConfigDictの基本形

ConfigDict・辞書・クラス引数の3つの書き方

公式のConfigurationの解説は、モデルへの設定方法として model_config 属性とクラス引数の2つを挙げ、model_config には通常の辞書も使えると書いています。3つとも同じ設定になることを確認します。

from pydantic import BaseModel, ConfigDict

class A1(BaseModel):
    model_config = ConfigDict(extra="forbid")
    a: int

class A2(BaseModel):
    model_config = {"extra": "forbid"}
    a: int

class A3(BaseModel, extra="forbid"):
    a: int

print(A1.model_config == A2.model_config == A3.model_config)  # True

差が出るのはつづりを間違えたときです。model_config 側で extra を extr と書いても、2.14.1は例外も警告も出さずにモデルを作り、余分な入力は既定の ignore で捨てられます。クラス引数で class M(BaseModel, extr="forbid") と書いた場合は、逆にクラス定義の時点で TypeError になります。

class Typo(BaseModel):
    model_config = ConfigDict(extr="forbid")  # extraのつづり誤り
    a: int

print(Typo(a=1, b=2))  # a=1(例外も警告も出ない)

同じ内容をmypy 2.4.0にかけると error: Extra key "extr" for TypedDict "ConfigDict" [typeddict-unknown-key] が出ました。model_config はConfigDict型として宣言されているため、辞書で書いた場合も同じエラーになります。model_config の設定ミスを拾えるのは型チェッカだけなので、ConfigDictを使うプロジェクトではCIで型チェックを回すのが前提になります。

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

v1では設定を内部クラス Config に書きました。v2でも動作しますが、クラス定義の時点で PydanticDeprecatedSince20 が発生します。

import warnings

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.14/migration/
# model_config -> {'extra': 'forbid'}

警告文のとおり削除予定はV3.0です。class Config の中身は model_config に変換されて効いているので、移行は同じキーと値をConfigDictへ移すだけで済みます。ただしv1から名前が変わったキーがあり、公式の移行ガイドの一覧では orm_mode は from_attributes、validate_all は validate_default、anystr_strip_whitespace は str_strip_whitespace、allow_population_by_field_name は populate_by_name(2.11以降は validate_by_name)に置き換えます。

継承でのマージと多重継承の優先順位

サブクラスに書いた model_config は親の設定とマージされ、同じキーだけが上書きされます。

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}

プロジェクト共通の設定を1つの基底クラスにまとめ、全モデルにそれを継承させる書き方は、公式も設定を全体に効かせる方法として案内しています。注意が要るのは多重継承です。公式ドキュメントは「複数の親を持つ場合、Pydanticは現時点ではMROに従わない」と注記しており、2.14.1で確かめると、後ろに書いた親の設定が勝ちました。

class Forbid(BaseModel):
    model_config = ConfigDict(extra="forbid")

class Allow(BaseModel):
    model_config = ConfigDict(extra="allow")

class M1(Forbid, Allow):
    x: int

class M2(Allow, Forbid):
    x: int

print(M1.model_config, M2.model_config)
# {'extra': 'allow'} {'extra': 'forbid'}

Pythonのメソッド解決順序なら M1 は先に書いた Forbid が優先されるはずなので、直感と逆の結果です。設定を持つ親を2つ以上継承する設計は避け、共通設定は単一の基底クラスに集めてください。

ネストした子モデルには設定が伝わらない

親モデルの設定は、フィールドとして持つ子モデルには適用されません。公式はこれを「各モデルが自分の設定の境界を持つ」と説明しています。

class Item(BaseModel):
    name: str

class Order(BaseModel):
    model_config = ConfigDict(extra="forbid")
    item: Item

print(Order(item={"name": "a", "color": "red"}))
# item=Item(name='a')  子モデルの余分なキー color は黙って捨てられる

APIの入力を厳密にしたくて最上位のモデルにだけ extra="forbid" を付けると、ネストした部分の未知キーやタイプミスは素通りします。入力スキーマ全体を締めるなら、前の節の共通基底クラスに extra="forbid" を書き、子モデルもそれを継承させるのが確実です。なお標準ライブラリのdataclassやTypedDictをフィールドに使った場合は、それ自身に設定が無ければ親の設定が伝わります。

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

よく使うオプションの既定値と、違反時に返るエラー種別(e.errors()[0]["type"])を2.14.1で確認した結果です。既定値は公式のConfigDictリファレンスの記載と一致しました(strict と from_attributes はリファレンスに既定値の明記が無く、実行時の挙動で確認)。

オプション 既定値 変えたときの効果 違反時のtype
extra ‘ignore’ 未定義キーを拒否/保持 extra_forbidden
strict False 文字列の数字などの型変換を止める int_type など
frozen False 代入を禁止し__hash__を生成する frozen_instance
validate_assignment False 代入時にも検証する int_parsing など
from_attributes False オブジェクトの属性から読む model_type
validate_default False 既定値も検証する int_parsing など
validate_by_alias True Falseで別名での入力を拒否する missing
validate_by_name False フィールド名での入力を受け付ける missing
serialize_by_alias False 出力のキーを別名にする なし

なお、何も設定していないモデルの model_config は空の辞書です。既定値は辞書に書き込まれず、Pydantic内部で補われます。

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

既定の ignore では、定義していないキーを渡してもエラーにならず、値は捨てられます。forbid にすると extra_forbidden で拒否します。

from pydantic import 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': 'https://errors.pydantic.dev/2.14/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'}

外部から受け取るリクエストボディのように、クライアントのタイプミスを早く検出したい入力には forbid を使います。外部APIのレスポンスを読む側は、相手がフィールドを追加しただけで処理が落ちないよう、既定の ignore のままにします。allow は中身の分からない追加データを素通しで転送したい場合に限られます。

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

既定のlaxモードは、文字列の "5" を整数の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はモデル全体に効くため、フォームやクエリ文字列のように値が文字列で届く入力に付けると正当な値まで弾きます。JSON本文の型をそのまま守らせたい内部APIに向いた設定です。

frozenとvalidate_assignmentの使い分け

frozen=True は生成後の代入を禁止し、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'

最後の例のとおり、既定の設定では int と宣言したフィールドに文字列が入ります。生成後に値を書き換えるモデルは validate_assignment=True を付けるか、書き換えないなら frozen=True にして代入そのものを禁止してください。

from_attributesでORMオブジェクトからモデルを作る

SQLAlchemyのモデルのように属性で値を持つオブジェクトは、既定では model_validate に渡せません。v1の orm_mode に当たるのが from_attributes です。

class Row:
    def __init__(self):
        self.id = 1
        self.name = "alice"

class UserOut(BaseModel):
    id: int
    name: str

try:
    UserOut.model_validate(Row())
except ValidationError as e:
    print(e.errors()[0]["type"])  # model_type

print(UserOut.model_validate(Row(), from_attributes=True))  # id=1 name='alice'

class UserORM(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    id: int
    name: str

print(UserORM.model_validate(Row()))  # id=1 name='alice'

呼び出しごとに from_attributes=True を渡す方法と、model_config に書く方法の結果は同じです。ORMの行を返すレスポンス用モデルは設定に書き、まれにしか使わない変換は呼び出し側で指定すると、意図がコードに残ります。コンストラクタ UserORM(Row()) は位置引数を受け付けず TypeError になるので、変換は必ず model_validate を通します。ORMとモデルを1つのクラスにまとめる方法は、SQLModelのモデル定義とテーブル作成|unique制約・CRUD・リレーション・FastAPI連携【0.0.42】で扱っています。

文字列・Enum・既定値の扱いを変えるオプション

入力の前処理に使うオプションです。str_strip_whitespace と str_to_lower は文字列の前後の空白除去と小文字化、use_enum_values はEnumの代わりに値を保持、coerce_numbers_to_str は数値から文字列への変換を許可します。

import enum

class Color(enum.Enum):
    RED = "red"

class Opt(BaseModel):
    model_config = ConfigDict(
        str_strip_whitespace=True,
        str_to_lower=True,
        use_enum_values=True,
        coerce_numbers_to_str=True,
    )
    name: str
    color: Color
    code: str

o = Opt(name="  ALICE ", color="red", code=123)
print(repr(o.name), repr(o.color), repr(o.code))  # 'alice' 'red' '123'

class Def(BaseModel):
    n: int = "x"  # 型と合わない既定値

class DefV(BaseModel):
    model_config = ConfigDict(validate_default=True)
    n: int = "x"

print(repr(Def().n))  # 'x'(既定値は検証されない)
try:
    DefV()
except ValidationError as e:
    print(e.errors()[0]["type"])  # int_parsing

coerce_numbers_to_str が無いと、str 型のフィールドに123を渡した時点で string_type エラーになります。商品コードや郵便番号が数値でも文字列でも届くような入力で使う設定です。use_enum_values には落とし穴があり、公式リファレンスは「Enum型のフィールドに既定値がある場合、既定値にも効かせるには validate_default が必要」と注記しています。既定値は検証を通らないため、Def の例のように型と合わない値もそのまま残ります。

エイリアスの設定:populate_by_nameからvalidate_by_nameへ

populate_by_nameの現状と2.14でのmodel_configの変化

populate_by_name=True は、別名(alias)とフィールド名のどちらでも入力を受け付ける設定です。2.14.1でも警告なしに動きます。

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

print(A(userId=1), A(user_id=2))  # user_id=1 user_id=2
print(A.model_config)             # {'populate_by_name': True}

2.13.5で同じコードを実行すると、最後の行は {'populate_by_name': True, 'validate_by_alias': True, 'validate_by_name': True} でした。2.14のリリース記事は「model_configはPydanticに書き換えられなくなり、常に利用者が書いた内容(親クラスの設定とのマージ結果)を表す」と説明しています。Model.model_config["validate_by_name"] のように展開後のキーを読んでいたコードは、2.14で KeyError になります。入力の受け付け方そのものは変わっていません。

公式リファレンスは populate_by_name について「v2.11以降は使用を推奨せず、v3で非推奨にする」と明記し、代わりに validate_by_name を使うよう案内しています。validate_by_name=True と validate_by_alias=True の組み合わせが、populate_by_name=True と厳密に同じ動作です。

validate_by_nameとvalidate_by_aliasの組み合わせ

2.11で入った2つのオプションは独立に切り替えられます。validate_by_alias=False にすると、別名での入力は受け付けません。

import inspect

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',)

print(inspect.signature(C))  # (*, user_id: int) -> None

最後の行は2.14の変更点です。2.13までは validate_by_alias=False でもシグネチャに別名の userId が出ており、help() やIPythonの補完など実行時にシグネチャを読むツールの表示と、実際に受け付ける引数が食い違っていました。2.14ではフィールド名で表示されます。なお、2つとも False にするとどちらの名前でも入力できなくなるため、モデル定義の時点でエラーになります。

alias_generatorとto_camelでcamelCaseのJSONを扱う

フロントエンドとcamelCaseのJSONでやり取りしつつ、Python側はsnake_caseで書きたい場合は、フィールドごとに alias を書かずに alias_generator で一括変換します。

from pydantic.alias_generators import to_camel

class Api(BaseModel):
    model_config = ConfigDict(
        alias_generator=to_camel,
        validate_by_name=True,
        validate_by_alias=True,
        serialize_by_alias=True,
    )
    user_id: int
    created_at: str

a = Api.model_validate({"userId": 1, "createdAt": "2026-10-21"})
print(a)  # user_id=1 created_at='2026-10-21'
print(Api(user_id=2, created_at="x").model_dump())
# {'userId': 2, 'createdAt': 'x'}

serialize_by_alias=True を書かないと、model_dump() はsnake_caseのキーで出力し、毎回 by_alias=True を渡す必要があります。公式リファレンスによると、このオプションは2.11で追加され、v3では既定値が True に変わる見込みです。いまのうちに明示しておけば、v3へ上げたときに出力のキーが変わる事故を防げます。別名を持ちながらsnake_caseで出力しているモデルは、逆に serialize_by_alias=False を明示しておきます。

入力の検証とValidationErrorの扱い

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

コンストラクタはキーワード引数を、model_validate は辞書などのオブジェクトを1つ受け取ります。外部から届いたJSON文字列は、json.loads で辞書にせず model_validate_json へ直接渡します。公式のPerformanceの解説も、最初の項目で model_validate(json.loads(...)) ではなく model_validate_json() を使うよう勧めています。

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

model_validate には strict や from_attributes、context を呼び出しごとに渡せます。モデルの設定は変えずに、特定の経路だけ厳しくしたいときに使います。

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

errors() はエラーごとに loc(どのフィールドか)、type(機械可読な種別)、msg(人向けの文)を持つ辞書のリストを返します。

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

画面に出すメッセージを切り替えるときは、msg の文面ではなく type で分岐してください。文面はバージョンで変わりえますが、type は公式のエラーの解説で「機械可読な識別子」と定義されています。

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'>

使ってよいのは、自分のデータベースから読み出した直後など、検証済みだと保証できるデータを大量に組み立てる場面だけです。外部入力に使うと、extra="forbid" の拒否や strict、型変換など、ここまで見た検証の設定はどれも効きません。

model_dumpとmodel_dump_jsonによる出力制御

model_dump() はPythonのオブジェクトのまま辞書にし、mode="json" を付けるとJSONに出せる型(文字列など)へ変換します。model_dump_json() はJSON文字列を直接返します。

import datetime

class D(BaseModel):
    at: datetime.datetime

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

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

v1の dict() と json() はそれぞれ model_dump() と model_dump_json() に置き換わりました。別名で出力するかどうかを毎回の引数で決めるか、前章の serialize_by_alias でモデルに固定するかは、そのモデルの出力先が1つかどうかで決めます。

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

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

conint や constr はv2でも動きますが、公式は Annotated と Field の組み合わせを推奨し、conint などはPydantic 3.0で非推奨にすると予告しています。生成されるJSON Schemaは同じです。

from typing 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'}

conint(...) は型ヒントの位置で関数を呼ぶ書き方なので、mypyなどの型チェッカは中身を int として扱えません。Annotated なら型は int のままで、制約だけがメタデータとして付きます。

field_validatorとmodel_validatorの役割分担

1つのフィールドの値を確かめるのが field_validator、複数フィールドの関係を確かめたり補ったりするのが model_validator です。v1の validator と root_validator に当たります。

from pydantic import field_validator, model_validator

class Order2(BaseModel):
    qty: int
    unit_price: int
    total: int | None = 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(Order2(qty=3, unit_price=100))
# qty=3 unit_price=100 total=300

mode="after" のバリデータは型変換が済んだインスタンスを受け取るので、計算は整数同士で行えます。変換前の生の入力を見たいときだけ mode="before" を使います。

Optional(int | None)のNone許容とフィールド省略の条件

v2では「Noneを入れられる」ことと「省略できる」ことが別の条件になりました。Optional[int] と同じ意味の int | None を付けても、既定値を書かなければ必須のままです。

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

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

v1では Optional を付けると暗黙に既定値Noneが入ったため、v1のコードをそのまま移すと必須のフィールドが増えます。公式の移行ガイドも「Required, optional, and nullable fields」の節でこの違いを扱っています。なお、可変な既定値はインスタンスごとに複製されるので、リストを既定値に書いても共有されません。

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

t1, t2 = Tags(), Tags()
t1.tags.append("a")
print(t1.tags, t2.tags)  # ['a'] []

pydantic-settingsのSettingsConfigDictとextraの既定値

環境変数や .env を読む設定クラスは、v2で別パッケージの pydantic-settings に分かれました。設定は SettingsConfigDict に書きます。ここで見落としやすいのが、BaseSettings の extra の既定値が 'forbid' である点です。

from pydantic_settings import BaseSettings, SettingsConfigDict

print(BaseSettings.model_config["extra"])  # forbid

class AppSettings(BaseSettings):
    model_config = SettingsConfigDict(env_prefix="APP_", env_file=".env")
    port: int = 8000

# .env の中身: APP_PORT=9000 と DEBUG=true の2行
# 環境変数 APP_UNKNOWN=1 を設定して実行
try:
    print(AppSettings())
except ValidationError as e:
    print(e.errors()[0]["type"], e.errors()[0]["loc"])
# extra_forbidden ('debug',)

pydantic-settings 2.16.1で実行すると、.env にある対応フィールドの無い DEBUG が extra_forbidden で拒否されました。一方、同じく対応フィールドの無い環境変数 APP_UNKNOWN は接頭辞が一致していても無視され、エラーになりません。公式のSettings Managementも、extraは渡された入力を検証するもので os.environ の全キーは見ないと説明しています。つまり環境変数名のつづり誤りは検出されず、既定値のまま動きます。.env を他のツールと共有していて無関係な行がある場合は、SettingsConfigDict に dotenv_filtering="match_prefix" を書くと、extra は forbid のまま接頭辞 APP_ の付いた行だけを読ませられます(上の例は port=9000 で起動)。extra="ignore" にする方法もありますが、その場合は .env のつづり誤りも黙って捨てられます。

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

移行で最初に手を入れるのは、class Config を model_config = ConfigDict(...) に書き換える箇所と、parse_obj・dict()・json() を model_validate・model_dump()・model_dump_json() に置き換える箇所です。いずれもPydanticの非推奨警告(PydanticDeprecatedSince20)が出るので、pytestを -W error::pydantic.warnings.PydanticDeprecatedSince20 付きで回すと、他のライブラリの警告は通したまま残りを失敗として洗い出せます。-W error::DeprecationWarning にすると、無関係なライブラリの警告まで失敗になります。手順の全体は公式の移行ガイドにまとまっています。

2.14でPython 3.9が対象外になった影響

2.14.0(2026年10月8日公開)はPython 3.9のサポートを終了し、3.15に対応しました。PyPIの登録情報では、2.14.1の requires_python は >=3.10、2.13.5は >=3.9 です。3.9の環境ではpipが3.9に対応する最後の2.13系を選ぶため、エラーにならずに古い版が入ります。2.14の変更(前述の model_config の扱いなど)を前提にしたコードは、3.9のCIでは別の版で動いていることになるので、CIのPythonの版を先に3.10以上へ上げてください。3.10以上なら、この記事の例のように Optional[int] を int | None と書けます(typing.List を list と書く形は3.9から使えます)。

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

公式の移行ツールbump-pydanticは、PyPIの最終版が0.8.0(2023年12月28日)で、GitHubのリポジトリは2026年5月27日にアーカイブされました。変換できるのは定型的な書き換えに限られるので、使う場合も差分を読み、残りは非推奨警告とテストで直します。

v2パッケージには旧APIを使える pydantic.v1 名前空間が同梱されています。

import pydantic, pydantic.v1 as v1

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

モジュール単位で import pydantic.v1 as v1 に読み替えれば、残りを後回しにして段階的に移せます。ただしV1の公式ドキュメントは「最後にサポートするPythonは3.13で、V1はPython 3.14以降では動かない」と明記しています。1.10.25で3.14への最小限の対応が入り、手元の3.14.6でも単純なモデルと class Config の検証は動きましたが、サポート対象外であることに変わりはありません。Python 3.14へ上げる予定があるなら、pydantic.v1 に残したコードの移行を先に終えてください。

GenericModelからBaseModelとGenericへの移行

v1の pydantic.generics.GenericModel は、v2では BaseModel と typing.Generic の多重継承に置き換わりました。

from typing import Generic, 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

ページング付きのAPIレスポンスのように、包む側の形が共通で中身の型だけが変わるモデルに使います。FastAPIのレスポンスモデルとして使う全体像はFastAPIとは|最短で動かす手順とasync/defの使い分け【2026年版】で扱っています。

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

V2の設計を公表した公式記事(2022年7月10日)は、pydantic-coreのベンチマークで「V1.9.1より4倍から50倍速く、一般的なフィールドを含むモデルの検証ではおよそ17倍速い」と書いています。倍率はモデルの形で大きく変わるため、2.14.1と同梱のpydantic.v1 1.10.26を同一プロセスで計測しました。同梱のV1は compiled が False のPython実装で、単体配布のCythonコンパイル版との比較ではありません。

  • 環境:macOS(Intel Core i9-9880H)、Python 3.12.13、pydantic 2.14.1、pydantic-core 2.50.1、pydantic.v1 1.10.26
  • 対象:id・name・price・tags を持つ子モデルを10件ネストした注文モデル
  • 計測:timeit.repeat でN=20,000回を3回繰り返した最小値。スクリプトを2回実行し、表は1回目の値(比の括弧内は2回目)
import timeit, json
import pydantic
import pydantic.v1 as v1

class C2(pydantic.BaseModel):
    id: int; name: str; price: float; tags: list[str]

class O2(pydantic.BaseModel):
    order_id: int; items: list[C2]

class C1(v1.BaseModel):
    id: int; name: str; price: float; tags: list[str]

class O1(v1.BaseModel):
    order_id: int; items: list[C1]

data = {"order_id": 1, "items": [{"id": i, "name": f"item{i}", "price": "9.5",
                                  "tags": ["a", "b"]} for i in range(10)]}
js = json.dumps(data)
o2, o1 = O2.model_validate(data), O1.parse_obj(data)
N = 20000

def t(f):
    return min(timeit.repeat(f, number=N, repeat=3)) / N * 1e6  # 1回あたりのマイクロ秒

print(t(lambda: O2.model_validate(data)), t(lambda: O1.parse_obj(data)))
print(t(lambda: O2.model_validate_json(js)), t(lambda: O1.parse_raw(js)))
print(t(lambda: o2.model_dump_json()), t(lambda: o1.json()))
処理 V2 V1 比
辞書の検証 40.3us 312.2us 7.7倍(8.3倍)
JSON文字列の検証 49.5us 357.3us 7.2倍(7.4倍)
JSON出力 24.3us 358.2us 14.7倍(12.8倍)

検証は7倍から8倍、JSON出力は13倍から15倍でした。2026年9月にPython 3.9.6と2.13.5で同じ形のモデルを測ったときは辞書の検証が12.9倍でした。そのときは計測方法(timeitの合計をNで割った値)もPythonとPydanticの版も違うので単純には比べられませんが、V2の時間は41.0usとほぼ同じで、V1の時間が528.7usから312.2usに縮んでいます。倍率は環境で大きく動くということです。

速度だけを理由に移行を計画するなら、本番相当のモデルで検証時間と件数を先に測るべきです。測らなくても移行を正当化できる理由は別にあり、class Config や旧メソッドがV3.0で削除される予定であること、pydantic.v1 がPython 3.14以降をサポートしないこと、ConfigDictの設定ミスを型チェッカで拾えることの3点です。

APIの入出力設計からPydanticのモデル定義まで外部に任せたい場合は、API開発・システム連携の支援で相談を受けています。LLMの出力をPydanticのモデルで検証する使い方は、LLM構造化出力とは?JSON Schemaで出力形式を保証する仕組みと実装・見送り条件を解説【2026年版】で扱っています。

よくある質問

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

2.14.1でも動きますが、クラス定義の時点で PydanticDeprecatedSince20 の警告が出て、V3.0で削除予定と明記されています。中身は model_config に変換されて効いているので、同じキーと値を model_config = ConfigDict(...) に移せば警告は消えます。orm_mode など名前の変わったキーだけは新しい名前に直してください。

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

モデルに定義していないキーが入力に含まれていたら、extra_forbidden のValidationErrorで拒否する設定です。既定の extra="ignore" では余分なキーが黙って捨てられます。ただし効くのはそのモデル自身だけで、フィールドとして持つ子モデルには伝わらないため、子モデルにも同じ設定が必要です。

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

2.14.1でも警告なしに動き、別名とフィールド名の両方で入力を受け付けます。ただし公式は「v2.11以降は推奨せず、v3で非推奨にする」と明記しているため、新規のコードは validate_by_name=True と validate_by_alias=True を書いておくと、v3で非推奨になったときに書き換えずに済みます。2.14からは model_config に展開後のキーが書き足されなくなった点にも注意してください。

model_configという名前のフィールドは定義できますか?

定義できません。2.14.1で model_config: str というフィールドを書くと、クラス定義の時点で PydanticUserError(model_config cannot be used as a model field name)になります。外部のJSONに同じ名前のキーがある場合は、Pythonのフィールド名を別にして Field(alias="model_config") で受け取ります。

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

できます。v2パッケージには pydantic.v1 名前空間が同梱されており、2.14.1には1.10.26が入っています。import pydantic.v1 as v1 に読み替えれば旧APIのまま動くので、モジュール単位で順に移せます。ただしV1はPython 3.14以降を公式にサポートしていないため、恒久的な置き場所にはせず、Pythonを上げる前に移行を終えてください。

関連記事

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

一創の公式 SNS

X では毎朝の新着記事と IT ニュースの速報を、YouTube・TikTok・Instagram・LinkedIn・Facebook では記事の解説動画を配信しています。

この記事は以下の記事からリンクされています

資料請求

今日のトレンド記事 直近 24 時間で、いつもより多く読まれている記事

  1. 2026.11.09 テックブログ エディオン不正アクセス第2報:カード番号の対象確認とログ・決済画面の改ざん対策
  2. 2026.11.09 テックブログ LANSCOPE不正アクセス第2報:製品への影響と偽連絡の確認、停止中のライセンス認証の窓口
  3. 2026.11.10 テックブログ Claude Fable 5.2とは:料金・破壊的変更4点とFable 5.1からの移行手順【2026年11月】
  4. 2026.11.09 テックブログ ステップゴルフ不正アクセス:口座情報の対象確認と偽連絡・引き落としの注意点
  5. 2026.11.10 テックブログ ニトリの情報漏洩:最大3,762万件の対象確認となりすまし連絡の見分け方

RELATED POSTS 関連記事

目次