Skip to Content
GuidesApp Design GuideSerializers

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
NamePurpose
AgentListSerializerLightweight fields for list views
AgentDetailSerializerFull fields for retrieve
AgentCreateSerializerInput validation for create
AgentUpdateSerializerInput validation for update
AgentStatusSerializerCustom 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 = fields

Detail — 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 = fields

Create 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 split

Without 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 = fields

From 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 = fields

Source 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 = fields

From 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 = fields

Wrapper 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 = fields

From 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 attrs

File 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 serializers

When the app has many models, each serializer file covers one model with all its variants (List, Detail, Create, Update).

Last updated on