Chapter 4: serialization. Pydantic → dict / JSON / arbitrary forms.
model_dump
user = User(id=1, email="[email protected]", name="Alice")
user.model_dump()
# {"id": 1, "email": "[email protected]", "name": "Alice", ...}
Returns Python types (datetime, UUID, etc. stay as objects).
model_dump(mode=“json”)
user.model_dump(mode="json")
# {"id": 1, "email": "[email protected]", "name": "Alice", "created_at": "2026-05-09T10:00:00Z"}
Converts datetime / UUID / Decimal / etc. to JSON-friendly forms (ISO strings, etc.).
model_dump_json
user.model_dump_json()
# '{"id":1,"email":"[email protected]","name":"Alice","created_at":"..."}'
Returns a JSON string. Faster than json.dumps(user.model_dump()) because Pydantic’s serializer is Rust.
exclude / include
user.model_dump(exclude={"created_at"})
user.model_dump(include={"id", "email"})
user.model_dump(exclude={"profile": {"address"}}) # nested
Drop fields from output.
exclude_none / unset / defaults
user.model_dump(exclude_none=True) # drops None values
user.model_dump(exclude_unset=True) # drops fields not explicitly set
user.model_dump(exclude_defaults=True) # drops fields equal to default
For PATCH-style “send only the changed fields”:
user.model_dump(exclude_unset=True)
by_alias
class User(BaseModel):
full_name: str = Field(alias="fullName")
user = User.model_validate({"fullName": "Alice"})
user.model_dump() # {"full_name": "Alice"}
user.model_dump(by_alias=True) # {"fullName": "Alice"}
For round-tripping JSON with externalKey conventions.
field_serializer
from pydantic import BaseModel, field_serializer
from datetime import datetime
class Event(BaseModel):
when: datetime
@field_serializer("when")
def serialize_when(self, v: datetime, _info) -> str:
return v.isoformat()
Custom output for one field. _info carries context.
For per-mode:
@field_serializer("when", when_used="json")
def to_iso(self, v, _info):
return v.isoformat()
when_used: “always”, “json”, “json-unless-none”, “unless-none”.
model_serializer
from pydantic import model_serializer
class User(BaseModel):
id: int
email: str
@model_serializer
def serialize(self) -> dict:
return {"user_id": self.id, "user_email": self.email}
Override the whole model’s serialization. Most apps don’t need this.
Round-tripping
user = User(id=1, email="[email protected]")
data = user.model_dump()
restored = User.model_validate(data)
assert restored == user
Standard pattern. Use mode="json" for JSON-safe round-trip:
data = user.model_dump(mode="json")
restored = User.model_validate(data) # ISO string → datetime
Computed fields in output
class User(BaseModel):
first: str
last: str
@computed_field
@property
def full_name(self) -> str:
return f"{self.first} {self.last}"
User(first="Alice", last="X").model_dump()
# {"first": "Alice", "last": "X", "full_name": "Alice X"}
Serialized; not accepted on input.
Aliases in output
class User(BaseModel):
full_name: str = Field(serialization_alias="fullName")
User(full_name="Alice").model_dump(by_alias=True)
# {"fullName": "Alice"}
Custom serialization for types
from pydantic import BaseModel
from datetime import datetime
from typing import Annotated
from pydantic import PlainSerializer
UnixTimestamp = Annotated[
datetime,
PlainSerializer(lambda dt: int(dt.timestamp()), return_type=int, when_used="json"),
]
class Event(BaseModel):
when: UnixTimestamp
Reusable serializer.
WrapSerializer
from pydantic import WrapSerializer
def conditional(value, handler, info):
if info.context and info.context.get("verbose"):
return {"value": handler(value), "ts": time.time()}
return handler(value)
class M(BaseModel):
name: Annotated[str, WrapSerializer(conditional)]
Wraps the default serializer. Context-dependent output.
Nested serialization
Nested Pydantic models serialize automatically:
class Profile(BaseModel):
address: str
class User(BaseModel):
profile: Profile
User(profile=Profile(address="...")).model_dump()
# {"profile": {"address": "..."}}
Lists / dicts of models
class Group(BaseModel):
members: list[User]
g = Group(members=[User(...), User(...)])
g.model_dump()
# {"members": [{"id": 1, ...}, {"id": 2, ...}]}
Performance
Pydantic v2’s serializer is Rust. model_dump_json() is significantly faster than:
json.dumps(user.model_dump())
Use the built-in for hot paths.
Bytes output
user.model_dump_json().encode()
# or
user.__pydantic_serializer__.to_json(user) # bytes directly
For network protocols.
Excluding sensitive fields
class User(BaseModel):
id: int
email: str
password_hash: str = Field(..., exclude=True)
exclude=True on Field permanently excludes from serialization. Useful for ORM-derived models that include sensitive columns.
Or use a separate Read model (preferred):
class UserDB(BaseModel):
id: int
email: str
password_hash: str # internal
class UserRead(BaseModel):
id: int
email: str # API output
Common mistakes
1. Returning ORM model directly
return user where user has password_hash. Use a Read model.
2. dict() instead of model_dump()
v1 API. Use model_dump().
3. JSON encoding manually
json.dumps(user.model_dump()) works but is slow. Use model_dump_json().
4. Forgetting mode=‘json’
{"created_at": datetime(...)} not JSON-serializable. Use mode="json" for JSON output.
5. Inconsistent alias use
by_alias=True on output but not handling aliases on input. Be consistent or populate_by_name=True.
What’s next
Chapter 5: Nested models, generics, discriminated unions.
Read this next
Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .