Skip to content

Serialization Interview Questions & Answers

13 questions Updated 2026-06-20 Share:

FastAPI Pydantic serialization interview questions — model_dump, model_dump_json, aliases, computed fields, custom serializers and JSON encoding.

13 of 13

Call .model_dump() (Pydantic v2). It replaces the v1 .dict() method.

from pydantic import BaseModel

class Item(BaseModel):
    name: str
    price: float
    in_stock: bool = True

item = Item(name="Widget", price=9.99)
item.model_dump()
# {"name": "Widget", "price": 9.99, "in_stock": True}

Key keyword arguments:

  • exclude_none=True — drop None values
  • exclude_unset=True — drop fields not explicitly set
  • exclude={"field"} — drop specific fields
  • include={"field"} — keep only specific fields
  • mode="json" — convert to JSON-safe types (datetime → str, UUID → str)

Rule of thumb: use model_dump(mode="json") before storing to NoSQL or passing to JSONResponse(content=...) — it handles non-serialisable types.

.model_dump_json() serialises directly to a JSON string (bytes) without going through a Python dict intermediate. It's faster because Pydantic uses its Rust core to serialise.

item = Item(name="Widget", price=9.99)
item.model_dump_json()
# b'{"name":"Widget","price":9.99,"in_stock":true}'

# round-trip: parse from JSON string
Item.model_validate_json('{"name":"Widget","price":9.99}')

Use model_dump_json() when writing to a cache, message queue, or file where you need a JSON string directly.

Rule of thumb: use model_dump() when you need a Python dict (to merge, modify, or pass to another dict); use model_dump_json() when you need a string/bytes.

By default, aliases affect input (parsing) only. To use aliases during output (serialisation), pass by_alias=True to model_dump():

from pydantic import BaseModel, Field

class Order(BaseModel):
    order_id: int = Field(alias="orderId")
    item_count: int = Field(alias="itemCount")

order = Order.model_validate({"orderId": 42, "itemCount": 3})

order.model_dump()              # {"order_id": 42, "item_count": 3}  (Python names)
order.model_dump(by_alias=True) # {"orderId": 42, "itemCount": 3}   (aliases)

In FastAPI, set response_model_by_alias=True in the decorator to use aliases in the HTTP response:

@app.get("/orders/{id}", response_model=Order, response_model_by_alias=True)

Rule of thumb: if your API contract uses camelCase, set by_alias=True globally in serialisation; don't mix Python names and aliases in the same API response.

Use @field_serializer("field_name") to override the default serialisation for a single field:

from pydantic import BaseModel, field_serializer
from datetime import datetime

class Event(BaseModel):
    name: str
    created_at: datetime

    @field_serializer("created_at")
    def serialise_dt(self, v: datetime) -> str:
        return v.strftime("%Y-%m-%d %H:%M")   # custom format instead of ISO 8601

e = Event(name="Launch", created_at=datetime(2026, 6, 20, 9, 0))
e.model_dump()
# {"name": "Launch", "created_at": "2026-06-20 09:00"}

Rule of thumb: use @field_serializer when you need a non-default format for dates, decimals, or custom types; prefer standard ISO 8601 datetimes unless the client explicitly requires a different format.

@model_serializer replaces the entire default serialisation of a model with a custom function. The function receives self (the model) and must return a JSON-serialisable value.

from pydantic import BaseModel, model_serializer

class Money(BaseModel):
    amount: int     # stored in cents
    currency: str

    @model_serializer
    def to_dict(self):
        return {
            "display": f"{self.amount / 100:.2f} {self.currency}",
            "cents": self.amount,
        }

m = Money(amount=999, currency="USD")
m.model_dump()
# {"display": "9.99 USD", "cents": 999}

Rule of thumb: use @model_serializer only when the serialised shape differs fundamentally from the model's fields (e.g., presenting a monetary value as display text + machine value).

exclude_unset=True returns only the fields the client explicitly sent, skipping fields that were left at their defaults. This is the correct behaviour for a PATCH — you only update what was provided.

class ItemUpdate(BaseModel):
    name: str | None = None
    price: float | None = None
    in_stock: bool | None = None

@app.patch("/items/{id}")
async def patch_item(id: int, patch: ItemUpdate):
    updates = patch.model_dump(exclude_unset=True)
    # client sent {"price": 14.99}
    # updates = {"price": 14.99}  — name and in_stock NOT included
    await db.update(id, updates)
    return await db.get(id)

Without exclude_unset=True, updates would include {"name": None, "price": 14.99, "in_stock": None}, accidentally overwriting existing values with None.

Rule of thumb: always use model_dump(exclude_unset=True) in PATCH handlers — it prevents accidental nulling of fields the client didn't mention.

In Pydantic v2 with mode="json" or model_dump_json(), standard types like datetime, UUID, Decimal, and Enum are handled automatically.

For custom third-party types, use @field_serializer:

from decimal import Decimal
from pydantic import BaseModel, field_serializer

class Price(BaseModel):
    amount: Decimal

    @field_serializer("amount")
    def encode_decimal(self, v: Decimal) -> str:
        return str(v)   # "9.99" instead of Decimal("9.99")

Price(amount=Decimal("9.99")).model_dump(mode="json")
# {"amount": "9.99"}

Rule of thumb: run model_dump(mode="json") in tests to check that all fields are JSON-serialisable — catch TypeError early before it surfaces in production.

  1. Handler returns a Python value (dict, ORM object, or Pydantic model).
  2. FastAPI calls response_model.model_validate(value) to filter and coerce it.
  3. The validated Pydantic instance is passed to jsonable_encoder() which calls model.model_dump(mode="json") internally.
  4. The resulting JSON-safe dict is serialised to bytes with json.dumps() (or orjson.dumps() if ORJSONResponse is configured).
  5. Bytes are sent as Content-Type: application/json.
# simplified internal equivalent
validated = ResponseModel.model_validate(handler_return)
payload   = jsonable_encoder(validated)   # → JSON-safe dict
body      = json.dumps(payload).encode()   # → bytes

Rule of thumb: understanding this flow explains why response_model filters extra fields (step 2) and why datetime values arrive as ISO strings (step 3).

Nested BaseModel instances are serialised recursively. .model_dump() returns nested dicts; .model_dump_json() returns a flat JSON string.

class Address(BaseModel):
    city: str
    country: str

class User(BaseModel):
    name: str
    address: Address

u = User(name="Alice", address=Address(city="London", country="UK"))
u.model_dump()
# {"name": "Alice", "address": {"city": "London", "country": "UK"}}

exclude and include work recursively:

u.model_dump(exclude={"address": {"country"}})
# {"name": "Alice", "address": {"city": "London"}}

Rule of thumb: test serialisation of nested models explicitly — a missing from_attributes=True on an inner model is a common bug when using ORM objects.

Call .model_dump() on each element, or use a RootModel for a top-level list:

items = [Item(name="A", price=1.0), Item(name="B", price=2.0)]
[i.model_dump() for i in items]
# [{"name": "A", "price": 1.0}, {"name": "B", "price": 2.0}]

In FastAPI, returning a list[Item] from a handler with response_model=list[Item] handles this automatically.

For a root-level list model:

from pydantic import RootModel

class ItemList(RootModel[list[Item]]):
    pass

ItemList([Item(name="A", price=1.0)]).model_dump()
# [{"name": "A", "price": 1.0}]

Rule of thumb: let FastAPI handle list serialisation via response_model=list[MyModel]; use RootModel only when you need to attach methods or validators to the list itself.

Use .model_copy(update={...}) (v2 replacement for .copy(update=...)):

original = Item(name="Widget", price=9.99, in_stock=True)
updated = original.model_copy(update={"price": 14.99})

print(original.price)  # 9.99   — unchanged
print(updated.price)   # 14.99  — new copy

This is useful in PATCH handlers after merging the update dict with the existing DB row's values:

db_item = await db.get(id)
pydantic_item = ItemOut.model_validate(db_item)
merged = pydantic_item.model_copy(update=patch.model_dump(exclude_unset=True))

Rule of thumb: use model_copy(update=...) instead of mutating model attributes directly — it keeps models immutable and makes the data flow explicit.

By default, Pydantic serialises an Enum to its .value. String enums (str, Enum) serialise as plain strings; int enums as integers.

from enum import Enum
from pydantic import BaseModel

class Status(str, Enum):
    active = "active"
    inactive = "inactive"

class User(BaseModel):
    status: Status

User(status=Status.active).model_dump()
# {"status": "active"}   — the string value, not the Enum object

If you need the enum member name instead of value:

user.model_dump(mode="python")   # {"status": <Status.active: 'active'>}

Rule of thumb: always inherit from str (or int) when defining enums for Pydantic models — it ensures JSON-safe serialisation without extra config.

Use json_schema_extra in model_config to add or override schema properties:

from pydantic import BaseModel, ConfigDict

class Item(BaseModel):
    model_config = ConfigDict(
        json_schema_extra={
            "title": "Inventory Item",
            "examples": [
                {"name": "Widget", "price": 9.99}
            ],
        }
    )
    name: str
    price: float

For programmatic customisation (add/remove properties):

model_config = ConfigDict(
    json_schema_extra=lambda schema: schema.update({"deprecated": True})
)

Rule of thumb: use json_schema_extra to add examples and title to your models — Swagger UI renders examples in the "Try it out" body, saving testers time.

More ways to practice

The self-quiz is live. Get notified when mock interviews and new question packs drop.

or
Join our WhatsApp Channel