Serializers¶
Standalone API serializer layer for shaping request/response data. Supports read/write separation, nested serialization, computed fields, validation, and type coercion.
Quick Start¶
from hyperdjango.serializers import Serializer, SerializerField
class UserSerializer(Serializer):
id: int = SerializerField(read_only=True)
username: str = SerializerField(min_length=1, max_length=150)
email: str = SerializerField(max_length=254)
password: str = SerializerField(write_only=True, min_length=8)
# Serialize (object -> dict for API response)
serializer = UserSerializer(obj={"id": 1, "username": "alice", "email": "a@b.com", "password": "hashed"})
data = serializer.data
# {"id": 1, "username": "alice", "email": "a@b.com"} -- password excluded (write_only)
# Deserialize (input dict -> validated data)
serializer = UserSerializer(input_data={"username": "alice", "email": "a@b.com", "password": "secret123"})
if serializer.is_valid():
clean = serializer.validated_data
# {"username": "alice", "email": "a@b.com", "password": "secret123"} -- id excluded (read_only)
else:
errors = serializer.errors
SerializerField Options¶
SerializerField(
read_only=False, # Include in output only (not accepted in input)
write_only=False, # Accept in input only (not in output)
required=True, # Must be present in input (ignored for read_only)
default=None, # Default value when missing from input
source="get_name", # Source attribute or method name on the object
min_length=None, # Minimum string length
max_length=None, # Maximum string length
min_value=None, # Minimum numeric value
max_value=None, # Maximum numeric value
choices=None, # Allowed values list
label=None, # Human-readable label (for OpenAPI)
help_text=None, # Description (for OpenAPI)
)
Computed Fields¶
Use the source parameter to point to a method on the serializer.
class UserSerializer(Serializer):
id: int = SerializerField(read_only=True)
first_name: str = SerializerField()
last_name: str = SerializerField()
full_name: str = SerializerField(read_only=True, source="compute_full_name")
def compute_full_name(self, obj):
return f"{obj.get('first_name', '')} {obj.get('last_name', '')}".strip()
serializer = UserSerializer(obj={"id": 1, "first_name": "Alice", "last_name": "Smith"})
serializer.data["full_name"] # "Alice Smith"
Nested Serializers¶
Annotate a field with another Serializer subclass for nested serialization.
class AddressSerializer(Serializer):
city: str = SerializerField()
country: str = SerializerField()
class UserSerializer(Serializer):
id: int = SerializerField(read_only=True)
name: str = SerializerField()
address: AddressSerializer = SerializerField(read_only=True)
user = {"id": 1, "name": "Alice", "address": {"city": "NYC", "country": "US"}}
serializer = UserSerializer(obj=user)
serializer.data
# {"id": 1, "name": "Alice", "address": {"city": "NYC", "country": "US"}}
Nested lists are handled automatically when the source value is a list.
Many Mode¶
Serialize/deserialize lists of objects.
users = [
{"id": 1, "username": "alice", "email": "a@b.com"},
{"id": 2, "username": "bob", "email": "b@b.com"},
]
serializer = UserSerializer(obj=users, many=True)
data = serializer.data # List of dicts
Validation¶
Field-Level Validation¶
Constraints are checked automatically during is_valid():
class ProductSerializer(Serializer):
name: str = SerializerField(min_length=1, max_length=200)
price: float = SerializerField(min_value=0)
status: str = SerializerField(choices=["draft", "active", "archived"])
serializer = ProductSerializer(input_data={"name": "", "price": -5, "status": "invalid"})
serializer.is_valid() # False
serializer.errors
# {"name": "Minimum length is 1", "price": "Minimum value is 0",
# "status": "Must be one of: ['draft', 'active', 'archived']"}
Cross-Field Validation¶
Override validate() for multi-field checks.
class DateRangeSerializer(Serializer):
start: str = SerializerField()
end: str = SerializerField()
def validate(self, data):
if data["start"] > data["end"]:
raise ValueError("start must be before end")
return data
Type Coercion¶
Input values are automatically coerced to match the annotated type:
| Annotation | Coercion |
|---|---|
int |
int(value) |
float |
float(value) |
bool |
String "true"/"1"/"yes" -> True |
str |
str(value) |
Invalid coercions produce an error (e.g., "abc" for int).
Partial Updates¶
Skip required field checks for PATCH-style updates.
serializer = UserSerializer(
input_data={"email": "new@example.com"},
partial=True, # Only validate fields that are present
)
if serializer.is_valid():
# validated_data only contains "email"
pass
Context¶
Pass extra data to computed fields via context.
serializer = UserSerializer(obj=user, context={"request": request})
# Access in compute methods: self.context["request"]
Inheritance¶
Serializer fields are inherited from parent classes.
class BaseSerializer(Serializer):
id: int = SerializerField(read_only=True)
created_at: str = SerializerField(read_only=True)
class UserSerializer(BaseSerializer):
username: str = SerializerField()
email: str = SerializerField()
# Inherits id and created_at from BaseSerializer
API Reference¶
Serializer¶
| Property/Method | Description |
|---|---|
.data |
Serialized output (dict or list for many=True) |
.is_valid() |
Validate input_data, returns bool |
.validated_data |
Validated input (call is_valid() first) |
.errors |
Validation errors dict (call is_valid() first) |
.validate(data) |
Override for cross-field validation |
Serializer Inheritance¶
class BaseSerializer(Serializer):
id: int = SerializerField(read_only=True)
created_at: str = SerializerField(read_only=True)
class ArticleSerializer(BaseSerializer):
title: str = SerializerField(max_length=200)
content: str = SerializerField()
# Inherits id and created_at from BaseSerializer
Field Subsetting¶
Control which fields appear in output:
class UserSerializer(Serializer):
id: int = SerializerField(read_only=True)
username: str = SerializerField()
email: str = SerializerField()
bio: str = SerializerField()
# Full serialization
full = UserSerializer(obj=user)
# {"id": 1, "username": "alice", "email": "a@b.com", "bio": "..."}
# For list views, use a separate lightweight serializer
class UserListSerializer(Serializer):
id: int = SerializerField(read_only=True)
username: str = SerializerField()
Partial Updates¶
serializer = ArticleSerializer(input_data={"title": "New Title"}, partial=True)
if serializer.is_valid():
# Only "title" in validated_data — other fields not required
await update_article(article_id, serializer.validated_data)
Computed Fields¶
class ArticleSerializer(Serializer):
id: int = SerializerField(read_only=True)
title: str = SerializerField()
summary: str = SerializerField(read_only=True, source="compute_summary")
def compute_summary(self, obj):
content = obj.get("content", "") if isinstance(obj, dict) else obj.content
return content[:100] + "..." if len(content) > 100 else content