Cheatsheet for the output side.

model_dump

user.model_dump()                          # Python types
user.model_dump(mode="json")               # JSON-safe types (datetime → str, etc.)
user.model_dump_json()                     # JSON string (Rust serializer; fast)

Filter

user.model_dump(exclude={"created_at"})
user.model_dump(include={"id", "email"})
user.model_dump(exclude={"profile": {"address"}})        # nested
user.model_dump(exclude_none=True)
user.model_dump(exclude_unset=True)
user.model_dump(exclude_defaults=True)
user.model_dump(by_alias=True)

field_serializer

from pydantic import 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()
    
    @field_serializer("when", when_used="json")
    def to_iso_json(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.

computed_field

from pydantic import computed_field

class User(BaseModel):
    first: str
    last: str
    
    @computed_field
    @property
    def full_name(self) -> str:
        return f"{self.first} {self.last}"

Serialized; not accepted on input.

Custom serialization via Annotated

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.

WrapSerializer

from pydantic import WrapSerializer

def conditional(v, handler, info):
    if info.context and info.context.get("verbose"):
        return {"value": handler(v), "ts": time.time()}
    return handler(v)

class M(BaseModel):
    name: Annotated[str, WrapSerializer(conditional)]

Excluded fields

class User(BaseModel):
    id: int
    email: str
    password_hash: str = Field(..., exclude=True)

exclude=True on Field — permanently excluded from serialization.

by_alias

class User(BaseModel):
    full_name: str = Field(serialization_alias="fullName")

User(full_name="Alice").model_dump(by_alias=True)
# {"fullName": "Alice"}

Round-trip

user = User(id=1, email="[email protected]")
js = user.model_dump_json()
restored = User.model_validate_json(js)

Bytes / JSON directly (fast)

user.__pydantic_serializer__.to_json(user)   # bytes directly (Rust)

For protocols where you want bytes, not str.

Pydantic vs json.dumps

# Slower
json.dumps(user.model_dump())

# Faster (Rust serializer)
user.model_dump_json()

Use the built-in for hot paths.

Nested

class Group(BaseModel):
    members: list[User]

g.model_dump()
# {"members": [{...}, {...}]}

Auto-recursive.

Per-context output

class User(BaseModel):
    id: int
    email: str

# For different audiences, use distinct schemas:
class UserPublic(BaseModel):
    id: int

class UserInternal(BaseModel):
    id: int
    email: str

Common mistakes

  • Returning ORM model directly in FastAPI without response_model — leaks fields.
  • dict() (v1) instead of model_dump().
  • json.dumps(model.model_dump()) for hot path — use model_dump_json().
  • mode="json" not set when datetime appears — Object of type datetime is not JSON serializable.

Read this next

If you want my pydantic + orjson FastAPI starter, it’s at rajpoot.dev .


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 .