Serializers
Serializers handle validation, field mapping, and output formatting. Each action should have its own serializer tailored to what it needs.
Naming Convention
{Model}{Action}Serializer| Name | Purpose |
|---|---|
AgentListSerializer | Lightweight fields for list views |
AgentDetailSerializer | Full fields for retrieve |
AgentCreateSerializer | Input validation for create |
AgentUpdateSerializer | Input validation for update |
AgentStatusSerializer | Custom action input/output |
Real file from agents (stockapis-trader) — views/serializers/agent_serializers.py has 4 serializer classes.
List vs Detail
List — minimal fields, fast queries:
class AgentListSerializer(serializers.ModelSerializer):
class Meta:
model = Agent
fields = [
"id", "agent_id", "exchange", "symbol",
"market_type", "is_active", "created_at",
]
read_only_fields = fieldsDetail — full fields, may include nested objects:
class AgentDetailSerializer(serializers.ModelSerializer):
memory_blocks = CoreMemoryBlockSerializer(many=True, read_only=True)
class Meta:
model = Agent
fields = [
"id", "agent_id", "exchange", "symbol", "market_type",
"is_active", "config", "memory_blocks", "created_at", "updated_at",
]
read_only_fields = fieldsCreate Serializer
Only include writable fields:
class AgentCreateSerializer(serializers.ModelSerializer):
class Meta:
model = Agent
fields = ["agent_id", "exchange", "symbol", "market_type", "config"]Read-Only for API Generation
Mark all fields as read-only in response serializers to prevent TypeScript type splitting:
class UserDetailSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ["id", "email", "name", "avatar", "created_at"]
read_only_fields = fields # Prevents Readable/Writable splitWithout read_only_fields = fields, the TypeScript generator may create both UserDetailReadable and UserDetailWritable types, breaking references to the base UserDetail type. See the DRF Guide for details.
Nested Serializers
For FK/M2M relations in read responses:
class DownloadRequestDetailSerializer(serializers.ModelSerializer):
symbol = SymbolNestedSerializer(read_only=True)
segments = DownloadSegmentSerializer(many=True, read_only=True)
class Meta:
model = DownloadRequest
fields = ["id", "symbol", "data_type", "status", "segments", "created_at"]
read_only_fields = fieldsFrom cryptodb_driver (stockapis) — the detail view includes both the parent symbol and all child segments.
Lightweight nested serializer
Create a focused serializer for nested display — don’t reuse the full serializer:
class SymbolNestedSerializer(serializers.ModelSerializer):
"""Minimal symbol info for embedding in other responses."""
class Meta:
model = Symbol
fields = ["id", "symbol", "exchange", "market_type"]
read_only_fields = fieldsSource Attribute
Map model relationships to flat output fields:
class ActivitySerializer(serializers.ModelSerializer):
workspace_name = serializers.CharField(source="workspace.name", read_only=True)
user_email = serializers.CharField(source="user.email", read_only=True)
class Meta:
model = Activity
fields = [
"id", "workspace_name", "user_email",
"type", "action", "details", "created_at",
]
read_only_fields = fieldsFrom activity (cmdop) — flattens workspace and user references into readable strings.
Computed Fields
For model properties that aren’t DB fields:
class DownloadRequestSerializer(serializers.ModelSerializer):
status = serializers.CharField(read_only=True) # @property
progress_percent = serializers.FloatField(read_only=True) # @property
total_records = serializers.IntegerField(
source="total_records_ingested", read_only=True
)
class Meta:
model = DownloadRequest
fields = ["id", "data_type", "status", "progress_percent", "total_records"]
read_only_fields = fieldsWrapper Response Serializer
For custom action responses that don’t map to a model:
class SymbolsByExchangeMarketResponseSerializer(serializers.Serializer):
"""Not tied to a model — just defines the response shape."""
exchange = serializers.CharField()
market_type = serializers.CharField()
count = serializers.IntegerField()
symbols = SymbolListSerializer(many=True)From symbols (stockapis) — used in @extend_schema(responses={200: ...}) to document the custom response shape.
One-Time Display Serializer
For data that should only be shown once (like raw API keys):
class APIKeyCreatedSerializer(serializers.ModelSerializer):
raw_key = serializers.CharField(read_only=True) # Only in creation response
class Meta:
model = APIKey
fields = ["id", "name", "prefix", "raw_key", "created_at"]
read_only_fields = fieldsFrom api_keys (stockapis) — the ViewSet returns this serializer only from create(), not from retrieve().
Validation
Field-level validation
class SyncRequestSerializer(serializers.Serializer):
exchange = serializers.ChoiceField(
choices=["binance", "bybit", "okx"],
required=False,
)
market_type = serializers.ChoiceField(
choices=["spot", "linear", "inverse"],
required=False,
)Object-level validation
class DownloadRequestCreateSerializer(serializers.ModelSerializer):
def validate(self, attrs):
if attrs["from_date"] > attrs["to_date"]:
raise serializers.ValidationError({
"from_date": "from_date must be before to_date"
})
return attrsFile Organization
One file per domain, named {model_name}_serializers.py:
views/serializers/
├── __init__.py
├── agent_serializers.py # All Agent-related serializers
├── memory_serializers.py # All Memory-related serializers
└── summary_serializers.py # All Summary-related serializersWhen the app has many models, each serializer file covers one model with all its variants (List, Detail, Create, Update).