# How to Create Custom Django REST Framework Serializers with OpenAPI Schema Annotations and help_text

> Learn to create custom Django REST Framework serializers with OpenAPI schema annotations and help_text for better documentation and TypeScript generation.

- Repository: [PostHog/posthog](https://github.com/PostHog/posthog)
- Tags: how-to-guide
- Published: 2026-04-25

---

**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`](https://github.com/PostHog/posthog/blob/main/products/web_analytics/backend/serializers.py), the `WoWChangeSerializer` demonstrates this pattern:

```python
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`](https://github.com/PostHog/posthog/blob/main/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`](https://github.com/PostHog/posthog/blob/main/products/tasks/backend/serializers.py) shows how to annotate a custom method that returns a nested dictionary:

```python
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`](https://github.com/PostHog/posthog/blob/main/products/web_analytics/backend/api.py), the `WebAnalyticsViewSet` implements a complete schema definition:

```python
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:

```bash
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`](https://github.com/PostHog/posthog/blob/main/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 descriptive `help_text` strings to populate the OpenAPI specification.
- **Use `@extend_schema_field`** on any `SerializerMethodField` method to declare the concrete return type, ensuring accurate TypeScript generation for computed fields.
- **Apply `@extend_schema`** to ViewSet actions to link query serializers, path parameters, and response serializers to the endpoint documentation.
- **Run `hogli build:openapi`** after 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`](https://github.com/PostHog/posthog/blob/main/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.