How to Create Custom Django REST Framework Serializers with OpenAPI Schema Annotations and help_text
Use explicit DRF field declarations with help_text for automatic documentation, and apply @extend_schema_field to SerializerMethodField methods to generate accurate TypeScript types from the backend serializers as the single source of truth.
In the PostHog/posthog repository, Django REST Framework serializers serve as the single source of truth for API responses consumed by the frontend. When you create custom serializers with schema annotations and help_text, you ensure that the generated OpenAPI specification produces accurate TypeScript definitions and documentation through the automated build pipeline.
Declaring Explicit Fields with help_text
PostHog requires every public serializer field to be declared explicitly with a concise help_text argument. This string is automatically extracted into the OpenAPI specification and becomes documentation for the TypeScript types that frontend developers consume.
In products/web_analytics/backend/serializers.py, the WoWChangeSerializer demonstrates this pattern:
from rest_framework import serializers
class WoWChangeSerializer(serializers.Serializer):
percent = serializers.IntegerField(
help_text="Absolute percentage change, rounded to nearest integer."
)
direction = serializers.ChoiceField(
choices=["Up", "Down"],
help_text="Direction of the change relative to the prior period."
)
color = serializers.CharField(
help_text="Hex color indicating whether the change is a positive or negative signal."
)
text = serializers.CharField(help_text="Short label, e.g. 'Up 12%'.")
long_text = serializers.CharField(
help_text="Verbose label, e.g. 'Up 12% from prior period'."
)
The help_text parameter in each field definition flows directly into the generated OpenAPI schema. According to the type-system documentation in docs/published/handbook/engineering/type-system.md, this approach ensures that frontend developers receive auto-completed documentation without manually maintaining parallel TypeScript interfaces.
Annotating Custom Fields with @extend_schema_field
When your serializer uses derived or computed data via SerializerMethodField, Django cannot infer the output type automatically. You must decorate the method with @extend_schema_field from drf-spectacular to specify the concrete JSON shape.
The Tasks product implementation in products/tasks/backend/serializers.py shows how to annotate a custom method that returns a nested dictionary:
from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers
class TaskSerializer(serializers.ModelSerializer):
latest_run = serializers.SerializerMethodField()
@extend_schema_field(serializers.DictField(
allow_null=True,
help_text="Latest run details for this task"
))
def get_latest_run(self, obj):
latest = obj.latest_run
if latest:
return TaskRunDetailSerializer(latest, context=self.context).data
return None
The @extend_schema_field decorator accepts either a primitive type from drf_spectacular.types.OpenApiTypes (such as OpenApiTypes.STR or OpenApiTypes.INT) or a reference to another serializer class. This annotation ensures that the OpenAPI generator emits the correct TypeScript type rather than defaulting to an opaque any type.
Documenting ViewSets with @extend_schema
To expose your serializer through an endpoint with properly typed query parameters and response schemas, apply the @extend_schema decorator to your ViewSet methods. This decorator bridges the gap between request parameters, response serializers, and the generated API documentation.
In products/web_analytics/backend/api.py, the WebAnalyticsViewSet implements a complete schema definition:
from drf_spectacular.utils import extend_schema, OpenApiParameter, OpenApiResponse
from rest_framework import serializers, viewsets
from rest_framework.decorators import action
class _DigestQuerySerializer(serializers.Serializer):
days = serializers.IntegerField(
min_value=1,
max_value=90,
required=False,
default=7
)
compare = serializers.BooleanField(required=False, default=True)
class WebAnalyticsViewSet(viewsets.GenericViewSet):
serializer_class = WeeklyDigestResponseSerializer
@extend_schema(
operation_id="web_analytics_weekly_digest",
summary="Summarize web analytics",
description="Summarizes a project's web analytics over a lookback window",
parameters=[
OpenApiParameter(
name="days",
type=OpenApiTypes.INT,
location=OpenApiParameter.QUERY,
required=False,
default=7,
description="Lookback window in days (1–90)."
),
OpenApiParameter(
name="compare",
type=OpenApiTypes.BOOL,
location=OpenApiParameter.QUERY,
required=False,
default=True,
description="Include period-over-period change."
)
],
responses={
200: OpenApiResponse(response=WeeklyDigestResponseSerializer)
},
tags=["web_analytics"],
)
@action(detail=False, methods=["get"], url_path="weekly_digest")
def weekly_digest(self, request, **kwargs):
qs = _DigestQuerySerializer(data=request.query_params)
qs.is_valid(raise_exception=True)
params = qs.validated_data
digest = build_team_digest(
self.team,
days=params["days"],
compare=params["compare"]
)
return Response(self.get_serializer(digest).data)
This pattern binds the _DigestQuerySerializer to query parameter validation while explicitly declaring the WeeklyDigestResponseSerializer as the success response type. Including operation_id, summary, and description improves the generated OpenAPI UI and produces clearer TypeScript client function names.
Regenerating Frontend Types
After modifying any serializer or schema annotation, you must regenerate the frontend TypeScript types to maintain synchronization between backend and frontend. PostHog uses a custom build command that triggers the OpenAPI generation pipeline:
hogli build:openapi
This command regenerates the OpenAPI specification and the accompanying TypeScript client types based on the current state of your serializers. As documented in the type-system handbook at docs/published/handbook/engineering/type-system.md (lines 62-70), running this step locally is mandatory before submitting changes, and CI enforces that the generated files remain in sync with the source serializers.
Summary
- Declare every field explicitly using DRF field classes (
CharField,IntegerField,ChoiceField) with descriptivehelp_textstrings to populate the OpenAPI specification. - Use
@extend_schema_fieldon anySerializerMethodFieldmethod to declare the concrete return type, ensuring accurate TypeScript generation for computed fields. - Apply
@extend_schemato ViewSet actions to link query serializers, path parameters, and response serializers to the endpoint documentation. - Run
hogli build:openapiafter any serializer change to update the generated TypeScript types and keep the frontend API client synchronized with the backend implementation.
Frequently Asked Questions
What happens if I forget to add help_text to a serializer field?
If you omit help_text, the field will still appear in the generated OpenAPI schema, but the documentation will be empty. This creates gaps in the TypeScript type definitions that frontend developers rely on for IDE auto-completion and inline documentation, reducing the developer experience and potentially causing confusion about the field's purpose.
Can I use @extend_schema_field with nested serializers instead of primitive types?
Yes. You can pass a serializer class directly to @extend_schema_field instead of a primitive type. For example, @extend_schema_field(TaskRunDetailSerializer) tells drf-spectacular to reference the schema of that nested serializer, generating the appropriate TypeScript interface with full property completion rather than a generic object type.
Where should I define query parameter serializers in PostHog?
Define query parameter serializers as private inner classes within the same file as your ViewSet, or as separate modules if shared across multiple endpoints. In products/web_analytics/backend/api.py, the _DigestQuerySerializer pattern demonstrates using a leading underscore to indicate that the serializer is internal to the API implementation and not exposed as a standalone response model.
Does PostHog require manual TypeScript interface updates alongside serializer changes?
No. PostHog treats Django serializers as the single source of truth for API types. You should never hand-write matching TypeScript interfaces. After updating your serializer with help_text and schema annotations, running hogli build:openapi automatically regenerates the TypeScript types, ensuring perfect parity between backend logic and frontend type safety.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →