開発

Pydantic model_dumpの使い方:exclude・mode・by_aliasとmodel_validateでのORM変換【2.14対応】

Pydantic model_dumpの使い方:exclude・mode・by_aliasとmodel_validateでのORM変換【2.14対応】

Pydanticのmodel_dump()は、モデルを辞書に書き出すメソッドです。逆向きに、辞書やORMの行からモデルを作るのがmodel_validate()です。この記事では、2026年11月時点の最新版pydantic 2.14.3を前提に、model_dump()の引数で出力を絞る方法、PATCHの部分更新でexclude_unsetを使う手順、SQLAlchemyの行をfrom_attributesで変換するときの落とし穴を、コードで示します。v1のdict()やparse_obj()からの置き換え表も載せています。

まとめ:model_dumpとmodel_validateで入出力をそろえる結論

APIの応答やファイルに書き出すならmodel_dump(mode="json")かmodel_dump_json()を使います。既定のmode="python"はdatetimeやDecimalをそのまま残すため、json.dumps()に渡すとTypeErrorで止まる点が落とし穴です。出さない項目は、毎回のexcludeではなくField(exclude=True)でモデルに固定する方が、出し忘れの事故を防げます。

部分更新ではexclude_unset=Trueで「送られてきた項目だけ」を取り出し、元の値と合わせてmodel_validate()で作り直します。model_copy(update=...)は値を検証しないためです。ORMの行はfrom_attributesで変換できますが、リレーションは変換前に読み込んでおかないと、セッションを閉じた後にエラーになります。

model_dumpの引数一覧とmode=”python”と”json”の出力の違い

最初に引数の全体像と、modeで出力の型がどう変わるかを押さえます。PyPIのpydanticでは、2026年11月14日時点の最新が2.14.3(11月5日公開・Python 3.10以上)です。

model_dumpとmodel_dump_jsonの引数と既定値を一覧で確認する

v2.14.3のmain.pyで、model_dump()の引数はすべてキーワード専用です。よく使うものを表にまとめます。

引数 既定値 役割
mode “python” “json”でJSON向けの型にする
include / exclude None 出す項目・出さない項目
by_alias None 別名をキーにする
exclude_unset False 渡されなかった項目を除く
exclude_defaults False 既定値と等しい項目を除く
exclude_none False Noneの項目を除く
exclude_computed_fields False computed_fieldを除く
context None シリアライザへ渡す値
warnings True 型の食い違いの扱い

model_dump_json()も同じ引数を持ち、加えてindentとensure_asciiを受け取ります。exclude_computed_fieldsと、呼び出しごとに型を判定し直すpolymorphic_serializationは比較的新しい引数で、同じリポジトリの変更履歴ではそれぞれ2.12と2.13で追加されています。古い版に固定している環境では使えません。

mode=”json”でdatetime・Decimal・setの出力が変わる型の対応表

from datetime import datetime
from decimal import Decimal
from uuid import UUID

from pydantic import BaseModel


class Order(BaseModel):
    order_id: UUID
    amount: Decimal
    tags: set[str]
    ordered_at: datetime


o = Order(
    order_id="6f1c2b1e-8a4d-4c3e-9b55-0d7c1e2f3a4b",
    amount="1280.50",
    tags={"gift"},
    ordered_at="2026-11-14T10:30:00",
)

print(o.model_dump())
# {'order_id': UUID('6f1c2b1e-8a4d-4c3e-9b55-0d7c1e2f3a4b'), 'amount': Decimal('1280.50'),
#  'tags': {'gift'}, 'ordered_at': datetime.datetime(2026, 11, 14, 10, 30)}

print(o.model_dump(mode="json"))
# {'order_id': '6f1c2b1e-8a4d-4c3e-9b55-0d7c1e2f3a4b', 'amount': '1280.50',
#  'tags': ['gift'], 'ordered_at': '2026-11-14T10:30:00'}

公式のSerializationの解説のとおり、mode="json"ではJSONで表せる型に変換されます。主な型の変わり方は次のとおりです。

フィールドの型 python(既定) json
datetime / date そのまま ISO形式の文字列
Decimal そのまま 文字列
UUID そのまま 文字列
set / tuple そのまま リスト
ネストしたモデル 辞書 辞書

金額をDecimalで持つ場合、JSONでは数値ではなく文字列になる点に注意します。受け取る側が数値を前提にしているなら、後述のfield_serializerで出力の型を決めておくのが確実です。ネストしたモデルは、どちらのモードでも再帰的に辞書へ変換されます。

model_dump_jsonで日本語のエスケープとインデントを調整する指定

from pydantic import BaseModel


class Shop(BaseModel):
    name: str


s = Shop(name="一創")
print(s.model_dump_json())                   # {"name":"一創"}
print(s.model_dump_json(ensure_ascii=True))  # {"name":"一創"}
print(s.model_dump_json(indent=2))           # 2文字下げで改行した形

model_dump_json()のensure_asciiは既定がFalseなので、日本語はそのまま出力されます。標準ライブラリのjson.dumps()は既定がTrueで、\uから始まる表記に変わります。ログやファイルで日本語が読めない原因の多くは、model_dump()の結果をjson.dumps()に渡していることです。既定の出力は空白を含まない詰めた形で、人が読む用途ならindentを指定します。

include・excludeでネストしたモデルやリストの項目を絞る方法

includeとexcludeには、項目名の集合だけでなく、入れ子の辞書も渡せます。社内向けの値を応答から外すときによく使います。

辞書形式のexcludeでネスト先とリストの要素番号を指定する

from pydantic import BaseModel


class User(BaseModel):
    id: int
    name: str
    password_hash: str


class Item(BaseModel):
    sku: str
    cost: int   # 原価。社外に出さない
    price: int


class Order(BaseModel):
    user: User
    items: list[Item]


order = Order(
    user={"id": 1, "name": "taro", "password_hash": "x"},
    items=[{"sku": "A-1", "cost": 300, "price": 500}, {"sku": "B-2", "cost": 800, "price": 1200}],
)

print(order.model_dump(exclude={"user": {"password_hash"}, "items": {"__all__": {"cost"}}}))
# {'user': {'id': 1, 'name': 'taro'}, 'items': [{'sku': 'A-1', 'price': 500}, {'sku': 'B-2', 'price': 1200}]}

print(order.model_dump(include={"items": {0: {"sku"}}}))
# {'items': [{'sku': 'A-1'}]}

辞書の値に項目名の集合を書くとネスト先の項目を、整数のキーを書くとリストの何番目かを指定できます。"__all__"はリストの全要素に同じ指定を当てる書き方です。公式の解説では、-1で最後の要素も指定できます。ただし、除外の中で一部だけ戻すようなFalseによる反転は使えません。

Field(exclude=True)とexclude_ifで常に出さない項目を決める

from pydantic import BaseModel, Field


class Member(BaseModel):
    id: int
    login_id: str
    password_hash: str = Field(exclude=True)                       # どの呼び出しでも出さない
    point: int = Field(default=0, exclude_if=lambda v: v == 0)    # 0 のときだけ出さない


m = Member(id=1, login_id="taro", password_hash="x")
print(m.model_dump())                            # {'id': 1, 'login_id': 'taro'}
print(m.model_dump(include={"password_hash"}))   # {}

パスワードのハッシュや内部の管理番号のように、どの経路でも外に出してはいけない項目はField(exclude=True)で固定します。公式の解説によると、フィールドに書いた除外はメソッドのincludeより優先されるため、呼び出し側の指定ミスで漏れることがありません。値に応じて出し分けたいときは、2.12で加わったexclude_ifに判定の関数を渡します。Fieldの他の引数はPydantic Fieldの使い方と引数一覧にまとめています。

PATCHの入力で使い分ける未設定値・既定値・Noneの除外条件

名前が似た3つの除外オプションは、何を基準に除くかが異なります。取り違えると、部分更新で値を消せない不具合につながります。

3つの除外オプションを同じ入力に適用した場合の出力結果の比較表

from pydantic import BaseModel


class Profile(BaseModel):
    nickname: str
    bio: str | None = None
    theme: str = "light"


p = Profile(nickname="taro", theme="light")
print(p.model_fields_set)                    # {'nickname', 'theme'}(順序は不定)
print(p.model_dump(exclude_unset=True))      # {'nickname': 'taro', 'theme': 'light'}
print(p.model_dump(exclude_defaults=True))   # {'nickname': 'taro'}
print(p.model_dump(exclude_none=True))       # {'nickname': 'taro', 'theme': 'light'}
オプション 除く基準 上の例のtheme
exclude_unset 作成時に渡されていない 残る(渡した)
exclude_defaults 既定値と等しい 消える
exclude_none 値がNone 残る

exclude_unsetの判定に使われるのはmodel_fields_setで、作成時に明示的に渡した項目の集合です。既定値と同じ値でも、渡していれば残ります。公式の解説によると、作成後に属性へ代入した項目もこの集合に加わります。

PATCHで未送信の項目を除外して更新値を再検証する実装の手順

from pydantic import BaseModel


class Profile(BaseModel):
    nickname: str
    bio: str | None = None
    theme: str = "light"


class ProfilePatch(BaseModel):   # PATCH 用。全項目を任意にする
    nickname: str | None = None
    bio: str | None = None
    theme: str | None = None


stored = Profile(nickname="taro", bio="Pythonが好きです", theme="dark")

patch = ProfilePatch.model_validate({"bio": None})   # 自己紹介だけ消したい
changes = patch.model_dump(exclude_unset=True)       # {'bio': None}

updated = Profile.model_validate(stored.model_dump() | changes)
print(updated)   # nickname='taro' bio=None theme='dark'

ここでexclude_noneを使うと、「送られなかった」と「空にしたい(null)」の区別が付かず、bioを消せません。exclude_unsetなら、送られてきたnullだけが残ります。

合成にはmodel_copy(update=changes)も使えますが、公式のModelsの解説のとおり、updateの値は検証されません。クライアントが{"nickname": null}を送ると、strのはずの項目にNoneが入ったモデルができてしまいます。元の値と合わせた辞書をmodel_validate()に通せば、この入力は検証エラーとして止まります。

field_serializerとcomputed_fieldで出力の形を作り替える方法

型ごとの既定の変換で足りないときは、シリアライザで出力の形を決めます。入力側の検証はfield_validatorの使い方で扱っており、ここは出力側の話です。

field_serializerのwhen_usedでJSON出力時だけ形式を変える

from datetime import datetime

from pydantic import BaseModel, computed_field, field_serializer


class Invoice(BaseModel):
    subtotal: int
    issued_at: datetime

    @computed_field
    @property
    def tax(self) -> int:
        return int(self.subtotal * 0.1)   # 端数は切り捨て

    @field_serializer("issued_at", when_used="json")
    def to_jp_date(self, v: datetime) -> str:
        return v.strftime("%Y年%m月%d日")


inv = Invoice(subtotal=1000, issued_at="2026-11-14T09:00:00")
print(inv.model_dump())
# {'subtotal': 1000, 'issued_at': datetime.datetime(2026, 11, 14, 9, 0), 'tax': 100}
print(inv.model_dump(mode="json"))
# {'subtotal': 1000, 'issued_at': '2026年11月14日', 'tax': 100}
print(inv.model_dump(exclude_computed_fields=True))
# {'subtotal': 1000, 'issued_at': datetime.datetime(2026, 11, 14, 9, 0)}

v2.14.3のfunctional_serializers.pyでは、field_serializerのwhen_usedに"always"(既定)、"unless-none"、"json"、"json-unless-none"の4種類を指定できます。"json"にすると、Pythonの処理で使うmodel_dump()ではdatetimeのまま、画面やAPIに出すJSONでだけ和暦表記などに変えられます。

computed_fieldは、保存していない値を出力に足す仕組みです。既定でmodel_dump()に含まれるため、出力を入力として読み戻すと、存在しない項目として扱われます。読み戻す経路ではexclude_computed_fields=Trueで外す指定が必要です。なお、v1でモデルの設定に書いていたjson_encodersは、公式の移行ガイドで非推奨とされており、field_serializerかPlainSerializerに置き換えます。

サブクラスの項目が落ちる件とserialize_as_anyの使いどころ

from pydantic import BaseModel, SerializeAsAny


class Animal(BaseModel):
    name: str


class Dog(Animal):
    breed: str


class Owner(BaseModel):
    pet: Animal


owner = Owner(pet=Dog(name="pochi", breed="shiba"))
print(owner.model_dump())                        # {'pet': {'name': 'pochi'}}
print(owner.model_dump(serialize_as_any=True))   # {'pet': {'name': 'pochi', 'breed': 'shiba'}}


class OwnerAny(BaseModel):
    pet: SerializeAsAny[Animal]   # このフィールドだけ実際の型で出す

v2では、型注釈が親クラスのフィールドに子クラスのインスタンスを入れても、出力されるのは親クラスで宣言した項目だけです。公式の移行ガイドにも、v1からの挙動の変更点として記載されています。子クラスで足した項目を出したいなら、フィールド単位でSerializeAsAnyを付けるのが安全です。serialize_as_any=Trueは全項目に効くため、意図しない項目まで出る恐れがあります。2.13以降は、モデルの設定と呼び出しの両方でpolymorphic_serializationも指定できます。

model_validateとfrom_attributesでORMの行をモデルへ変換する

model_validate()は、辞書やオブジェクトを1つ受け取り、検証してモデルを作るクラスメソッドです。入力の形ごとに兄弟のメソッドがあります。

model_validateの引数とJSON・文字列辞書を検証するメソッドの選択

from pydantic import BaseModel, ValidationError


class Page(BaseModel):
    page: int
    size: int = 20


print(Page.model_validate({"page": "2"}))           # page=2 size=20(文字列を数値に変換)

try:
    Page.model_validate({"page": "2"}, strict=True)  # 呼び出しごとに厳密にする
except ValidationError as e:
    print(e.errors()[0]["type"])                    # int_type

print(Page.model_validate_json('{"page": 3}'))       # JSON 文字列はそのまま渡す
print(Page.model_validate_strings({"page": "4", "size": "50"}))   # 値がすべて文字列の入力

Page.model_validate({"page": 1, "debug": True}, extra="forbid")  # 余分な項目を拒否(2.12以降)

2.14.3のmodel_validate()は、strict、extra、from_attributes、context、by_alias、by_nameを受け取ります。モデルの設定を変えずに、特定の経路だけ扱いを変えたいときに使う引数です。model_validate_strings()はJSONモードで検証するため、CSVやクエリ文字列のように値がすべて文字列の入力を、日時や数値へ変換できます。モデル全体の設定として持たせる場合はConfigDictの設定一覧を参照してください。

SQLAlchemyのリレーションを変換するときの遅延ロードの罠

from_attributesを有効にすると、辞書ではなく属性で値を持つオブジェクトを変換できます。v1のorm_modeに当たる設定です。SQLAlchemy 2.0系の書き方で、ユーザーと注文の1対多を変換する例を示します。

from pydantic import BaseModel, ConfigDict
from sqlalchemy import ForeignKey, create_engine, select
from sqlalchemy.orm import DeclarativeBase, Mapped, Session, mapped_column, relationship, selectinload


class Base(DeclarativeBase):
    pass


class UserOrm(Base):
    __tablename__ = "users"
    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str]
    orders: Mapped[list["OrderOrm"]] = relationship(back_populates="user")


class OrderOrm(Base):
    __tablename__ = "orders"
    id: Mapped[int] = mapped_column(primary_key=True)
    user_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
    total: Mapped[int]
    user: Mapped[UserOrm] = relationship(back_populates="orders")


class OrderOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    id: int
    total: int


class UserOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    id: int
    name: str
    orders: list[OrderOut]


engine = create_engine("sqlite://")   # メモリ上の SQLite
Base.metadata.create_all(engine)

with Session(engine) as session:
    session.add(UserOrm(id=1, name="taro", orders=[OrderOrm(id=10, total=1280)]))
    session.commit()
    # 注文を先に読み込んでおく(selectinload を外すと、下の変換で失敗する)
    user = session.scalars(select(UserOrm).options(selectinload(UserOrm.orders))).one()

print(UserOut.model_validate(user).model_dump())
# {'id': 1, 'name': 'taro', 'orders': [{'id': 10, 'total': 1280}]}

from_attributesによる変換は、フィールド名と同じ属性を順に読みます。SQLAlchemyのリレーションは既定で遅延ロードなので、ordersを読んだ瞬間がSQLの発行タイミングです。セッションを閉じた後だと、SQLAlchemyのエラー解説(bhk3)にあるDetachedInstanceErrorで失敗します。セッションの中で変換を済ませるか、リレーションの読み込みガイドにあるselectinloadで先に読み込んでおきます。一覧の応答でユーザーごとにSQLが走るN+1問題を避ける意味でも、読み込み方は明示する方が安全です。

公式のModelsの解説の例では、ネストした側のモデルにもfrom_attributes=Trueを設定しています。応答用のモデルはまとめて設定しておくと迷いません。ORMとPydanticのモデルを1つのクラスにまとめる方法はSQLModelの使い方で扱っています。テーブル設計と合わせてAPIの入出力モデルを整理し直す改修は、データベース設計・移行支援でご相談を受け付けています。

ORMから変換したモデルではexclude_unsetが効かない理由

from_attributesで変換すると、属性として存在したフィールドはすべて「渡された値」としてmodel_fields_setに入ります。ORMの行は全列を属性に持つため、model_dump(exclude_unset=True)にしても何も除かれません。NULLの列を応答から外したいならexclude_none=True、既定値と同じ値を外したいならexclude_defaults=Trueを使います。exclude_unsetは、クライアントから届いた入力を扱うPATCHのような場面に限って使う、と覚えておきます。

v1の辞書・JSON・ORM変換メソッドをv2へ置き換える手順と差異

v1のメソッドは2.14.3でも残っていますが、PydanticDeprecatedSince20の非推奨警告の対象です。公式の移行ガイドの対応をまとめます。

v1メソッドとv2メソッドの対応表と書き換え時に確認する挙動の差

v1の書き方 v2の書き方
m.dict() m.model_dump()
m.json() m.model_dump_json()
m.copy() m.model_copy()
Model.parse_obj(d) Model.model_validate(d)
Model.parse_raw(s) Model.model_validate_json
Model.from_orm(o) Model.model_validate(o)
Config.orm_mode from_attributes
Config.json_encoders field_serializer

名前を置き換えるだけでは済まない箇所は、次に挙げる3つです。1つ目はfrom_orm()で、2.14.3のソースではfrom_attributesを設定していないモデルで呼ぶとPydanticUserErrorになります。2つ目はjson()で、移行ガイドによるとindentなどの引数を渡した場合は、分かりにくいエラーになる可能性があるため注意が必要です。3つ目はサブクラスの出力で、前の章のとおり親クラスの項目だけになります。

copy(include=…)とexclude付きの複製を書き換える方法

v2のmodel_copy()はupdateとdeepしか受け取らず、includeやexcludeはありません。2.14.3のソースにあるcopy()の説明では、次の形に書き換える方法が示されています。

# v1:order.copy(exclude={"items"}, update={"note": "再発行"})
data = order.model_dump(exclude={"items"}, round_trip=True)
data = {**data, "note": "再発行"}
copied = Order.model_validate(data)   # 複製し直すので値も検証される

round_trip=Trueは、書き出した値をそのまま入力として読み戻せる形にする指定です。複製し直す形なので、更新した値も検証の対象です。移行後のテストは-W error::DeprecationWarningを付けて走らせ、残ったdict()やparse_obj()を洗い出します。メソッド名の対応はBaseModelの入門であるPydanticの読み方・BaseModelの使い方とV1との違いでも一覧にしています。

よくある質問

model_dumpとmodel_validateについて出てくることの多い質問に、2026年11月時点の版(pydantic 2.14.3)で答えます。

model_dump()とdict(model)は何が違いますか?

dict(model)は、最上位のフィールド名と値の組を並べるだけです。ネストしたモデルはモデルのまま残り、excludeや別名、シリアライザも効きません。辞書の中まで変換したい、出力の規則をモデルの定義どおりに当てたい、という目的ならmodel_dump()を使います。

model_dumpのキーを別名(camelCase)にするにはどうすればよいですか?

呼び出しごとならmodel_dump(by_alias=True)、モデル全体の既定にするならConfigDictのserialize_by_alias=Trueです。FastAPIの応答は既定で別名を使うため、テストでmodel_dump()した結果とキー名が食い違うことがあります。仕組みはFieldの使い方の別名の章で説明しています。

FastAPIの応答で未送信の項目を省くにはどうすればよいですか?

パス操作のデコレータにresponse_model_exclude_unset=Trueやresponse_model_exclude_none=Trueを指定すると、応答を作るときに同じ名前の除外が効きます。ただし、ORMから変換したモデルでは前述のとおり全項目が「渡された値」になるため、exclude_unsetは効きません。リクエストの受け方と422の読み方はFastAPIでPOSTを受け取る方法で扱っています。

数千件のモデルをまとめてJSONにするときの書き方は?

リストをforで回して1件ずつmodel_dump()するより、TypeAdapter(list[OrderOut])を作ってdump_json()を呼ぶ方が、変換がpydantic-coreの中で1回で済みます。読み込みも同じで、JSON配列はvalidate_json()に文字列のまま渡す形です。TypeAdapterの生成には時間がかかるので、関数の中で毎回作らず、モジュールの最上位で1回だけ作ります。

model_dumpで「serialized value may not be as expected」と警告が出るのはなぜですか?

フィールドの型と実際の値が食い違っているときの警告です。validate_assignmentを有効にしていないモデルに後から別の型の値を代入した場合や、model_construct()やmodel_copy(update=...)で検証せずに値を入れた場合に起きます。warnings="error"を指定すると例外に変わるので、テストではこちらにして食い違いを早めに見つけます。

関連記事

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

一創の公式 SNS

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

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

資料請求

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

  1. 2026.11.13 テックブログ 宝くじ公式サイト不正アクセス:4,436名の対象確認とパスワード再設定、変更された仕組み
  2. 2026.11.13 テックブログ KARTEの不正アクセス:全機能停止で利用企業がタグ・配信・データ連携を確かめる手順
  3. 2026.11.14 テックブログ ヤマダデンキの不正アクセス:リフォーム管理システム最大350万件と、住所・見積を持つ営業記録の守り方
  4. 2026.10.21 テックブログ マネックス証券の不正アクセス:メールアドレス最大約113万件と本人確認画像1,592件の漏えいのおそれ、口座開設停止とPHP脆弱性との関係
  5. 2026.11.11 テックブログ Microsoft 2026年11月の月例更新:Outlookの悪用済みCVE-2026-124194と修正版の確認方法

RELATED POSTS 関連記事

目次