Type system guide

PostHog has two type generation systems that keep frontend and backend in sync. This guide covers both directions and best practices for each.

Overview

FlowSource of truthGenerated outputUsed for
Backend → FrontendDjango serializersTypeScript (Orval)API responses
Frontend → BackendTypeScript schema.tsPydantic schema.pyQuery types (HogQL, filters, insights), some legacy types

These are independent systems. Don't conflate them.

Backend → Frontend (API responses)

We use Orval to generate TypeScript types and API client functions from our OpenAPI schema.

Ownership

  • Backend owns response types – Generated from Django serializers via OpenAPI
  • Frontend owns request/query types – Handwritten types for queries, filters, UI state
  • Do not manually redefine backend response types in frontend code

Where types live

TypeLocationEditable?
Generated API typesproducts/<product>/frontend/generated/No
Core API typesfrontend/src/generated/core/No
Handwritten typesfrontend/src/types/Yes

Never edit files in generated/ – they're overwritten on regeneration.

Naming conventions

  • Generated schema types end with Api suffix: TaskApi, SurveyApi, DashboardApi
  • Operation response types follow Orval naming: tasksListResponse200, tasksCreateResponse201
  • Handwritten types never use the Api suffix

This prevents name collisions between generated and manual types.

Regenerating types

Run after changing serializers, viewsets, or @extend_schema decorators:

Terminal
hogli build:openapi

CI will fail if generated types are stale.

Adding a new product's API

  1. Ensure products/your_product/frontend/ directory exists
  2. Tag your viewset with ProductKey:
Python
from posthog.schema import ProductKey
from posthog.api.documentation import extend_schema
@extend_schema(tags=[ProductKey.YOUR_PRODUCT])
class YourViewSet(TeamAndOrgViewSetMixin, viewsets.ModelViewSet):
serializer_class = YourSerializer
  1. Run hogli build:openapi
  2. Types appear in products/your_product/frontend/generated/

Serializers are the source of truth for response types. Use explicit field types and help_text where helpful.

Documenting query parameters

For endpoints with query parameters, use @validated_request (WIP pattern):

Python
from posthog.api.utils import validated_request
class MyQuerySerializer(serializers.Serializer):
status = serializers.ChoiceField(choices=["active", "archived"], required=False)
limit = serializers.IntegerField(default=100, min_value=1)
@validated_request(
query_serializer=MyQuerySerializer,
responses={200: MyResponseSerializer(many=True)},
)
@action(methods=["GET"], detail=False)
def my_action(self, request, **kwargs):
status = request.validated_query_data.get("status") # Use validated data
...

This validates inputs AND documents the endpoint for OpenAPI. Use request.validated_query_data, not manual request.query_params parsing.

Troubleshooting

Types not generating? Check @extend_schema(tags=[ProductKey.YOUR_PRODUCT]) and that products/your_product/frontend/ exists.

Wrong type shapes? The serializer is the source of truth. Use @extend_schema_field for custom SerializerMethodField types.

CI failing? Run hogli build:openapi locally and commit the regenerated files.

Design decisions

Why commit generated files? Makes type changes visible in PRs, lets CI catch drift, avoids needing Django running for frontend builds.

Why Api suffix? Prevents collisions with handwritten types. If you see Api, it came from the backend.

Why no deduplication across products? Keeps products isolated—changing one serializer won't affect another's types.

Frontend → Backend (query types)

Query types like TrendsQuery, FunnelsQuery, and HogQL filters are defined in TypeScript and generated to Python.

How it works

  1. Source: frontend/src/queries/schema.ts (TypeScript interfaces)
  2. Intermediate: frontend/src/queries/schema.json (JSON Schema)
  3. Output: posthog/schema.py (Pydantic models)

Regenerating

Terminal
hogli build:schema

This runs:

  1. build:schema-json – TS → JSON Schema
  2. build:schema-python – JSON Schema → Pydantic

When to add types here

Add to schema.ts when you need a type that:

  • Is sent from frontend to backend as a query/filter
  • Needs validation on the backend
  • Is part of HogQL or insight definitions
  • Do not add types that are only used in the frontend UI
  • Do not add types just because you need a type in Python – use handwritten types for backend-only logic

If you need a type from the backend in the frontend, define it in serializers and use the backend → frontend generation system.

For backend-only types, define Pydantic models directly in your own product e.g. in a domain_types.py file.

Help clean up

We're migrating from manually-defined API types to generated ones and also clean up ownership. You can help:

Quick wins

  1. Find manual types that duplicate generated ones – Search for interfaces like Dashboard, Survey, FeatureFlag in frontend/src/types/ that now have generated DashboardApi, SurveyApi equivalents
  2. Replace API call return types – If you see api.get<ManualType>(...), switch to using the generated functions and types

When touching existing code

  • If a file imports manual types for API responses, consider migrating to generated types
  • Add adapter functions where the UI needs a different shape than the API provides
  • Don't mix manual and generated types for the same entity in the same file

Community questions

Was this page useful?

Questions about this page? or post a community question.