Pydantic Serialization: model_dump, JSON and Aliases
So far, data has been coming into our models. That is validation: outside data becomes a model. This lesson is about the opposite direction, serialization: a model becomes a dictionary or JSON text, ready to send to a browser, save in a file or pass to another system.
| Direction | Method | Result |
|---|---|---|
| model to dict | model.model_dump() | Python dictionary |
| model to JSON | model.model_dump_json() | JSON text (a str) |
| dict to model | Model.model_validate(data) | model |
| JSON to model | Model.model_validate_json(text) | model |
This lesson follows 04_serialization.py. All the examples in the first four sections use this Employee model, which contains a nested Address:
from datetime import date
from decimal import Decimal
from pydantic import BaseModel
class Address(BaseModel):
city: str
pin_code: str
class Employee(BaseModel):
id: int
first_name: str
last_name: str
email: str
salary: Decimal
joined_on: date
manager_id: int | None = None
is_active: bool = True
address: Address | None = None
employee = Employee(
id=1, first_name="Asha", last_name="Rao", email="asha@example.com", salary="75000.50",
joined_on="2024-04-01", address={"city": "Pune", "pin_code": "411001"},
)
1. model_dump(): model to dict
data = employee.model_dump()
print(data)
print("Type of salary in the dict:", type(data["salary"]).__name__) # Decimal stays Decimal
# mode="json" turns every value into a JSON-friendly type (Decimal/date -> str)
print(employee.model_dump(mode="json"))
- The nested
Addressmodel also became a dictionary.model_dump()converts everything, all the way down. - By default the values keep their Python types:
DecimalstaysDecimal,datestaysdate. That's what you want when you pass the data to other Python code, for example to create a database row. - With
mode="json", values become JSON-friendly:Decimalanddatebecome strings.
2. model_dump_json(): model to JSON text
print(employee.model_dump_json())
print(employee.model_dump_json(indent=2))
One call gives you valid JSON: None becomes null, True becomes true, and dates are written in the standard YYYY-MM-DD format. You don't need json.dumps(), and you won't get the famous "Object of type Decimal is not JSON serializable" error. indent=2 makes it readable for humans.
3. Choosing which fields to output
You rarely want to send every field. model_dump() and model_dump_json() take options to choose:
print("include :", employee.model_dump(include={"id", "first_name"}))
print("exclude :", employee.model_dump(exclude={"salary", "address"}))
print("nested exclude :", employee.model_dump(exclude={"address": {"pin_code"}}, include={"id", "address"}))
print("exclude_none :", employee.model_dump(exclude_none=True, include={"id", "manager_id"}))
print("exclude_defaults:", employee.model_dump(exclude_defaults=True))
| Option | Keeps / removes |
|---|---|
include={...} | Keeps only these fields |
exclude={...} | Removes these fields |
exclude={"address": {"pin_code"}} | Removes a field inside a nested model |
exclude_none=True | Removes fields whose value is None (manager_id was removed) |
exclude_defaults=True | Removes fields that still have their default value (manager_id, is_active) |
exclude_unset=True | Keeps only fields that were actually given when the model was created |
exclude_unset: very important for updates
changes = Employee.model_validate({"id": 1, "first_name": "A", "last_name": "B", "email": "e",
"salary": 1, "joined_on": "2024-01-01"})
print("exclude_unset :", changes.model_dump(exclude_unset=True))
print("fields that were set:", changes.model_fields_set)
manager_id, is_active and address were not in the input, so they are left out. Pydantic remembers which fields were set in model_fields_set.
Why does this matter? When a user edits only their phone number, you must update only the phone number, not overwrite every other column with defaults or None. exclude_unset=True gives you exactly the fields the user sent. We use this for real database updates in lesson 06.
4. Round trip: model to JSON and back
json_text = employee.model_dump_json()
again = Employee.model_validate_json(json_text)
print(again)
print("Same as the original?", again == employee)
model_validate_json() reads JSON text and validates it in one step. The text "75000.50" becomes a Decimal again, and "2024-04-01" becomes a date. Nothing is lost in the round trip.
5. Aliases: different names outside, Python names inside
Python uses snake_case names (order_id), but JavaScript front ends and many APIs use camelCase (orderId). An alias maps the outside name to your Python field name, so your Python code stays clean.
from pydantic import ConfigDict, Field
class Order(BaseModel):
model_config = ConfigDict(validate_by_name=True) # also accept the Python name
order_id: int = Field(alias="orderId") # input AND output name
customer_name: str = Field(alias="customerName")
total: float
order = Order.model_validate({"orderId": 501, "customerName": "Ravi", "total": 1250})
print(order) # inside Python: order_id, customer_name
print("by_alias=False:", order.model_dump())
print("by_alias=True :", order.model_dump(by_alias=True))
print("Python names also accepted:", Order(order_id=502, customer_name="Neha", total=10))
- The input used
orderId, but in Python you writeorder.order_id. - Output uses Python names by default;
by_alias=Truewrites the alias names. - Without
validate_by_name=True, only the alias (orderId) would be accepted as input. With it, both names work.
Different names for input and output
from pydantic import AliasChoices
# validation_alias = name used only when READING input
# serialization_alias = name used only when WRITING output
# AliasChoices = accept any of several input names
class Product(BaseModel):
product_id: int = Field(validation_alias=AliasChoices("id", "productId", "ProductID"))
name: str = Field(serialization_alias="productName")
print(Product.model_validate({"ProductID": 9, "name": "Pen"}).model_dump(by_alias=True))
print(Product.model_validate({"id": 10, "name": "Book"}).model_dump(by_alias=True))
This is handy when you receive data from several old systems that each spell the id differently (id, productId, ProductID). One model accepts them all.
6. alias_generator: camelCase for every field automatically
Writing an alias for every field is boring. An alias generator creates them automatically:
from pydantic.alias_generators import to_camel
class Invoice(BaseModel):
model_config = ConfigDict(alias_generator=to_camel, validate_by_name=True, serialize_by_alias=True)
invoice_no: str
customer_name: str
total_amount: float
due_date: date
invoice = Invoice.model_validate({"invoiceNo": "INV-1", "customerName": "Asha",
"totalAmount": 999.5, "dueDate": "2026-12-01"})
print(invoice.model_dump_json(indent=2)) # camelCase output (serialize_by_alias=True)
alias_generator=to_camelturns everysnake_casefield name intocamelCase.serialize_by_alias=Trueuses the aliases in the output automatically, so you don't needby_alias=Trueon every call.- Other generators:
to_pascal(InvoiceNo) andto_snake.
7. field_serializer, computed_field and hidden fields
Sometimes the output must look different from the stored value: money with a currency symbol, a date in "01-Apr-2024" format, a password that must never be sent. And sometimes you want to include values that are calculated from other fields.
from datetime import datetime
from pydantic import computed_field, field_serializer
class Staff(BaseModel):
first_name: str
last_name: str
salary: Decimal
joined_on: date
password: str = Field(exclude=True) # NEVER appears in the output
created_at: datetime = Field(default_factory=datetime.now)
@field_serializer("salary")
def format_salary(self, value: Decimal) -> str:
return f"Rs. {value:,.2f}"
@field_serializer("joined_on")
def format_date(self, value: date) -> str:
return value.strftime("%d-%b-%Y") # 01-Apr-2024
@field_serializer("created_at", when_used="json") # only for JSON output
def short_time(self, value: datetime) -> str:
return value.strftime("%Y-%m-%d %H:%M")
# computed_field = a calculated value that is included in the output
@computed_field
@property
def full_name(self) -> str:
return f"{self.first_name} {self.last_name}"
@computed_field
@property
def years_of_service(self) -> int:
return (date.today() - self.joined_on).days // 365
staff = Staff(first_name="Kiran", last_name="Desai", salary="125000", joined_on="2019-04-01", password="secret")
print(staff.model_dump())
print(staff.model_dump_json(indent=2))
print("password is still on the object:", staff.password)
Field(exclude=True):passwordis a normal field you can use in Python, but it is never included inmodel_dump()or JSON. A simple, safe way to stop secrets from leaking into API responses.@field_serializer("salary"): changes only how the value is written. Inside Python,staff.salaryis still aDecimalyou can calculate with.when_used="json": the custom format is used only for JSON. In the dict output,created_atstayed a realdatetime; in the JSON it is the short text.@computed_fieldon a@property: a value calculated from other fields and included in the output.full_nameandyears_of_serviceare not stored; they are calculated every time.
Exercises
From the end of 04_serialization.py:
- Make a model
Book(book_id, title, price, published_on)that reads{"BookId": 1, "Title": "...", "Price": 299, "PublishedOn": "2020-05-01"}(hint:alias_generator=to_pascalfrompydantic.alias_generators). - Print it as JSON with snake_case names and then with PascalCase names.
- Add a
computed_fieldprice_with_gst(price * 1.18). - Add a field
internal_codethat never appears inmodel_dump(). - Turn the JSON back into a Book with
model_validate_json().
Summary
model_dump()gives a dict (Python types);mode="json"ormodel_dump_json()gives JSON-friendly output.include,exclude,exclude_none,exclude_defaultsandexclude_unsetchoose which fields are written.exclude_unset=Truereturns only the fields that were sent, which is essential for updates.model_validate_json()reads and validates JSON in one step.- Aliases map outside names (camelCase) to Python names;
alias_generator=to_cameldoes it for every field. @field_serializerchanges how a value is written,@computed_fieldadds calculated values, andField(exclude=True)hides a field.
Next: converting SQLAlchemy database objects into Pydantic models.