.agents/skills/improving-drf-endpoints/references/serializer-fields.md
help_text flows through the entire pipeline to become the OpenAPI field description,
the Zod .describe() string, and ultimately what an LLM reads when deciding how to fill a parameter.
# Bad — agent has no idea what this field expects
name = serializers.CharField()
# Good — clear, actionable description
name = serializers.CharField(
help_text="Human-readable name for the action. Used in the UI and API responses."
)
Guidelines for writing help_text:
A bare ListField() produces z.unknown() in the generated Zod schema.
# Bad — Orval generates z.array(z.unknown())
tags = serializers.ListField()
# Good — generates z.array(z.string())
tags = serializers.ListField(
child=serializers.CharField(),
help_text="Tags to apply to this resource. Each tag is a plain string.",
)
# Good — typed child for complex items
steps = serializers.ListField(
child=ActionStepSerializer(),
help_text="Ordered list of action steps. Each step defines a match condition.",
)
Bare JSONField produces a generic object schema. The fix is a custom field class
with @extend_schema_field pointing to a Pydantic model or inline schema.
The canonical pattern is in posthog/api/alert.py:
from drf_spectacular.utils import extend_schema_field
from pydantic import BaseModel
# 1. Define the schema (Pydantic model or OpenAPI-compatible class)
class AlertCondition(BaseModel):
type: str
threshold: float
operator: str
# 2. Create a custom field class with the decorator
@extend_schema_field(AlertCondition) # type: ignore[arg-type]
class AlertConditionField(serializers.JSONField):
pass
# 3. Use the custom field in the serializer
class AlertSerializer(serializers.ModelSerializer):
condition = AlertConditionField(
required=False,
allow_null=True,
help_text="Condition that triggers the alert. See AlertCondition schema.",
)
For simpler cases where a full Pydantic model is overkill, use OpenApiTypes:
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import extend_schema_field
@extend_schema_field(OpenApiTypes.OBJECT)
class MetadataField(serializers.JSONField):
"""At minimum, this tells Orval it's an object, not unknown."""
pass
Without @extend_schema_field, drf-spectacular can't infer the return type.
# Bad — return type unknown in OpenAPI
class TeamSerializer(serializers.ModelSerializer):
member_count = serializers.SerializerMethodField()
def get_member_count(self, obj):
return obj.members.count()
# Good — explicit return type
class TeamSerializer(serializers.ModelSerializer):
member_count = serializers.SerializerMethodField()
@extend_schema_field(serializers.IntegerField(help_text="Number of team members"))
def get_member_count(self, obj):
return obj.members.count()
For complex return types:
@extend_schema_field(serializers.ListField(child=serializers.CharField()))
def get_redirect_uris_list(self, instance) -> list[str]:
return instance.redirect_uris.split() if instance.redirect_uris else []
# Bad — valid values not discoverable
status = serializers.CharField()
# Good — choices appear in OpenAPI enum
status = serializers.ChoiceField(
choices=["active", "archived", "deleted"],
help_text="Current status of the resource.",
)
# Bad — generic dict
properties = serializers.DictField()
# Good — typed values
properties = serializers.DictField(
child=serializers.CharField(),
help_text="Key-value pairs of event properties. Keys and values are strings.",
)