Serializers¶
Django-Bolt provides a powerful Serializer class built on top of msgspec.Struct. It offers field validation, computed fields, dynamic field selection, and Django model integration - all with excellent performance.
Why use Serializers?¶
In Django REST Framework, you often need multiple serializer classes for different views:
# DRF approach - multiple serializers
class UserListSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', 'username']
class UserDetailSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', 'username', 'email', 'created_at']
class UserAdminSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', 'username', 'email', 'created_at', 'is_staff']
With Django-Bolt's Serializer, you define one class and create views dynamically:
from django_bolt.serializers import Serializer
class UserSerializer(Serializer):
id: int
username: str
email: str
created_at: str
is_staff: bool = False
class Config:
field_sets = {
"list": ["id", "username"],
"detail": ["id", "username", "email", "created_at"],
"admin": ["id", "username", "email", "created_at", "is_staff"],
}
# Use different views from the same serializer
UserListSerializer = UserSerializer.fields("list")
UserDetailSerializer = UserSerializer.fields("detail")
Basic usage¶
Creating a serializer¶
from django_bolt.serializers import Serializer
class UserSerializer(Serializer):
username: str
email: str
Create instances just like dataclasses:
Converting to dict¶
user = UserSerializer(username="alice", email="alice@example.com")
data = user.to_dict()
# {'username': 'alice', 'email': 'alice@example.com'}
Using dump() for output¶
The dump() method serializes instances and respects configuration:
user = UserSerializer(username="alice", email="alice@example.com")
data = user.dump()
# {'username': 'alice', 'email': 'alice@example.com'}
Options for dump():
# Exclude None values
data = user.dump(exclude_none=True)
# Exclude default values
data = user.dump(exclude_defaults=True)
Default values¶
class UserSerializer(Serializer):
username: str
email: str = "no-email@example.com"
user = UserSerializer(username="bob")
print(user.email) # "no-email@example.com"
Optional fields¶
class UserSerializer(Serializer):
username: str
email: str | None = None
user = UserSerializer(username="alice")
print(user.email) # None
Field validation¶
The field_validator decorator¶
Use @field_validator to validate and transform individual fields:
from django_bolt.serializers import Serializer, field_validator
class UserSerializer(Serializer):
email: str
@field_validator("email")
def validate_email(cls, value):
if "@" not in value:
raise ValueError("Invalid email")
return value
Invalid data raises msgspec.ValidationError:
Transforming values¶
Validators can transform values before storage:
class UserSerializer(Serializer):
email: str
@field_validator("email")
def normalize_email(cls, value):
return value.lower().strip()
user = UserSerializer(email=" ALICE@EXAMPLE.COM ")
print(user.email) # "alice@example.com"
Multiple validators¶
Apply multiple validators to a single field:
class UserSerializer(Serializer):
password: str
@field_validator("password")
def check_length(cls, value):
if len(value) < 8:
raise ValueError("Password too short")
return value
@field_validator("password")
def check_complexity(cls, value):
if not any(c.isupper() for c in value):
raise ValueError("Password must have uppercase")
return value
Validators run in order. If the first fails, subsequent validators don't run.
Using msgspec.Meta for constraints¶
For declarative validation, use Annotated with msgspec.Meta:
from typing import Annotated
from msgspec import Meta
from django_bolt.serializers import Serializer
class AuthorSerializer(Serializer):
id: int
name: Annotated[str, Meta(min_length=2)]
email: Annotated[str, Meta(pattern=r"^[^@]+@[^@]+\.[^@]+$")]
Meta constraints are enforced during deserialization via msgspec.convert().
Model-level validation¶
The model_validator decorator¶
Use @model_validator for cross-field validation after all fields are set:
from django_bolt.serializers import Serializer, model_validator
class PasswordSerializer(Serializer):
password: str
password_confirm: str
@model_validator
def check_passwords_match(self):
if self.password != self.password_confirm:
raise ValueError("Passwords don't match")
Execution order¶
Field validators run first, then model validators:
class TestSerializer(Serializer):
value: str
@field_validator("value")
def field_val(cls, v):
print("Field validator")
return v
@model_validator
def model_val(self):
print("Model validator")
TestSerializer(value="test")
# Prints: "Field validator" then "Model validator"
Computed fields¶
Basic computed fields¶
Use @computed_field to add derived values to output:
from django_bolt.serializers import Serializer, computed_field
class UserSerializer(Serializer):
first_name: str
last_name: str
@computed_field
def full_name(self) -> str:
return f"{self.first_name} {self.last_name}"
user = UserSerializer(first_name="John", last_name="Doe")
result = user.dump()
# {'first_name': 'John', 'last_name': 'Doe', 'full_name': 'John Doe'}
Computed fields with aliases¶
@computed_field(alias="displayName")
def display_name(self) -> str:
return f"{self.first_name} {self.last_name}".upper()
Chaining computed fields¶
Computed fields can use other computed fields:
class ProductSerializer(Serializer):
price: float
quantity: int
@computed_field
def total(self) -> float:
return self.price * self.quantity
@computed_field
def formatted_total(self) -> str:
return f"${self.total():.2f}" # Call as method
Dynamic field selection¶
One of the most powerful features: create different views from a single serializer.
Using only()¶
Select specific fields:
class UserSerializer(Serializer):
id: int
name: str
email: str
created_at: str
user = UserSerializer(id=1, name="John", email="john@example.com", created_at="2024-01-01")
# Get only id and name
result = UserSerializer.only("id", "name").dump(user)
# {'id': 1, 'name': 'John'}
Using exclude()¶
Exclude specific fields:
Using field_sets with use()¶
Define reusable field sets in Config:
class UserSerializer(Serializer):
id: int
name: str
email: str
password: str
created_at: str
updated_at: str
class Config:
field_sets = {
"list": ["id", "name", "email"],
"detail": ["id", "name", "email", "created_at", "updated_at"],
"minimal": ["id", "name"],
}
# Use predefined field sets
list_result = UserSerializer.use("list").dump(user)
detail_result = UserSerializer.use("detail").dump(user)
Chaining field selection¶
view = UserSerializer.only("id", "name", "email").exclude("email")
result = view.dump(user)
# {'id': 1, 'name': 'John'}
Field selection with computed fields¶
Computed fields work with field selection:
class UserSerializer(Serializer):
first_name: str
last_name: str
@computed_field
def full_name(self) -> str:
return f"{self.first_name} {self.last_name}"
user = UserSerializer(first_name="John", last_name="Doe")
# Include computed field
result = UserSerializer.only("first_name", "full_name").dump(user)
# {'first_name': 'John', 'full_name': 'John Doe'}
Creating type-safe serializer subsets¶
The subset() method¶
Create actual subclasses with only specific fields:
class UserSerializer(Serializer):
id: int
name: str
email: str
password: str
# Create a type-safe subset
UserMiniSerializer = UserSerializer.subset("id", "name")
# UserMiniSerializer is a proper class
user = UserMiniSerializer(id=1, name="John")
The fields() method¶
Create subsets from field_sets:
class UserSerializer(Serializer):
id: int
name: str
email: str
class Config:
field_sets = {
"list": ["id", "name"],
"detail": ["id", "name", "email"],
}
UserListSerializer = UserSerializer.fields("list")
UserDetailSerializer = UserSerializer.fields("detail")
# These are proper subclasses with type annotations
assert issubclass(UserListSerializer, Serializer)
Converting from parent to subset¶
UserMini = UserSerializer.subset("id", "name")
# Create full instance
full_user = UserSerializer(id=1, name="John", email="john@example.com", password="secret")
# Convert to mini
mini_user = UserMini.from_parent(full_user)
print(mini_user.dump()) # {'id': 1, 'name': 'John'}
Write-only fields¶
Hide sensitive fields from output:
class UserCreateSerializer(Serializer):
email: str
password: str
class Config:
write_only = {"password"}
user = UserCreateSerializer(email="test@example.com", password="secret123")
result = user.dump()
# {'email': 'test@example.com'} # password excluded
Nested serializers¶
Basic nesting¶
class AddressSerializer(Serializer):
street: str
city: str
zip_code: str
class UserSerializer(Serializer):
id: int
name: str
address: AddressSerializer
address = AddressSerializer(street="123 Main St", city="NYC", zip_code="10001")
user = UserSerializer(id=1, name="John", address=address)
result = user.dump()
# {
# 'id': 1,
# 'name': 'John',
# 'address': {'street': '123 Main St', 'city': 'NYC', 'zip_code': '10001'}
# }
Lists of nested serializers¶
class TagSerializer(Serializer):
id: int
name: str
class PostSerializer(Serializer):
id: int
title: str
tags: list[TagSerializer]
tags = [TagSerializer(id=1, name="python"), TagSerializer(id=2, name="django")]
post = PostSerializer(id=1, title="Hello World", tags=tags)
Using Nested marker for Django models¶
The Nested marker provides explicit control over nested serialization:
from typing import Annotated
from django_bolt.serializers import Serializer, Nested
class AuthorSerializer(Serializer):
id: int
name: str
email: str
class BlogPostSerializer(Serializer):
id: int
title: str
author: Annotated[AuthorSerializer, Nested(AuthorSerializer)]
tags: Annotated[list[TagSerializer], Nested(TagSerializer, many=True)]
Django model integration¶
From model to serializer¶
Use from_model() to create serializer instances from Django models:
class ArticleSerializer(Serializer):
id: int
title: str
content: str
# From Django model
article = await Article.objects.aget(id=1)
serializer = ArticleSerializer.from_model(article)
With select_related¶
When using from_model() with ForeignKey relationships, use select_related:
# Without select_related - may cause N+1 queries
post = await BlogPost.objects.aget(id=1)
serializer = BlogPostSerializer.from_model(post) # author might be just an ID
# With select_related - nested object included
post = await BlogPost.objects.select_related("author").aget(id=1)
serializer = BlogPostSerializer.from_model(post) # author is full object
Bulk serialization¶
# Serialize multiple instances
users = [
UserSerializer(id=1, name="John"),
UserSerializer(id=2, name="Jane"),
]
result = UserSerializer.only("id", "name").dump_many(users)
# [{'id': 1, 'name': 'John'}, {'id': 2, 'name': 'Jane'}]
Serializer inheritance¶
Serializers support inheritance:
class BaseUserSerializer(Serializer):
username: str
email: str
@field_validator("email")
def validate_email(cls, value):
if "@" not in value:
raise ValueError("Invalid email")
return value
class AdminSerializer(BaseUserSerializer):
is_admin: bool = False
# Child inherits validators
admin = AdminSerializer(username="alice", email="alice@example.com", is_admin=True)
Built-in type aliases¶
Django-Bolt provides pre-defined type aliases for common patterns:
from django_bolt.serializers import (
# String lengths
Char50, Char100, Char255,
# Validated strings
Email, URL, Slug, UUID,
# Integers
PositiveInt, NonNegativeInt,
# Network
IPv4, IPv6, Port,
# Auth
Username, Password,
# And more...
)
class UserSerializer(Serializer):
username: Username # max 150 chars, validated pattern
email: Email # validated email format
website: URL | None # validated URL
Using with API handlers¶
from django_bolt import BoltAPI
from django_bolt.serializers import Serializer
api = BoltAPI()
class UserSerializer(Serializer):
id: int
username: str
email: str
class Config:
field_sets = {
"list": ["id", "username"],
"detail": ["id", "username", "email"],
}
UserListSerializer = UserSerializer.fields("list")
UserDetailSerializer = UserSerializer.fields("detail")
@api.get("/users")
async def list_users() -> list[UserListSerializer]:
users = []
async for user in User.objects.all()[:20]:
users.append(UserListSerializer.from_model(user))
return users
@api.get("/users/{user_id}")
async def get_user(user_id: int) -> UserDetailSerializer:
user = await User.objects.aget(id=user_id)
return UserDetailSerializer.from_model(user)
Performance tips¶
-
Use field selection for lists: Only include fields you need in list views.
-
Use select_related with from_model(): Prevent N+1 queries when serializing relationships.
-
Use subset() for type safety: Creates actual classes that editors can type-check.
-
Use write_only for sensitive data: Password fields should never appear in output.