资讯详情

PostHog DRF 端点工程规范:从 @validated_request 与 help_text 到全链路类型管线

📅 2026/9/10 21:18:52 | 华诺云谱 👁 阅读
PostHog DRF 端点工程规范:从 @validated_request 与 help_text 到全链路类型管线
PostHog DRF 端点工程规范从 validated_request 与 help_text 到全链路类型管线【免费下载链接】posthog:hedgehog: PostHog is the leading platform for building self-driving products. Our developer tools – AI observability, analytics, session replay, flags, experiments, error tracking, logs, and more – capture all the context agents need to diagnose problems, uncover opportunities, and ship fixes. Steer it all from Slack, web, desktop, or the MCP.项目地址: https://gitcode.com/GitHub_Trending/po/posthogPostHog 将 Django REST FrameworkDRF的 ViewSet 与 Serializer 视为整个类型系统的源头并以仓库级规则文件 .claude/rules/drf-endpoints.md 约束所有 API 代码任何 viewset 方法必须携带 schema 注解validated_request或extend_schema任何序列化器字段必须携带help_text。本文结合规则文件指向的improving-drf-endpoints技能SKILL.md 及其四份参考文档与源码实现完整讲解这三条铁律背后的工程动机、装饰器源码机制、字段规范、枚举命名、路由注册与 CI 校验帮助你在 PostHog 及任何以 OpenAPI 为契约中枢的 DRF 项目中写出可被前端类型、MCP 工具与 LLM Agent 正确消费的端点。规则从哪来drf-endpoints 规则的适用范围规则文件本身极短但约束力极强。它通过 YAML frontmatter 声明了生效路径然后给出三条硬性要求--- paths: - posthog/api/** - products/*/backend/api/** - products/*/backend/presentation/** ---生效范围posthog/api/**核心 API、products/*/backend/api/**各产品后端的 API 层、products/*/backend/presentation/**各产品后端的表现层视图。第一条在编辑这些目录下的任何 viewset 或 serializer 之前先调用/improving-drf-endpoints技能——即 .claude/skills/improving-drf-endpoints/SKILL.md该技能附带 serializer-fields.md、viewset-annotations.md、quick-reference-table.md、common-anti-patterns.md 四份参考文档。第二条每个 viewset 方法必须有 schema 注解validated_request或extend_schema。第三条每个 serializer 字段必须有help_text。从源码结构看这两条硬性规则是 PostHog 全链路类型管线的兜底闸门序列化器与注解的任何遗漏都会静默地向下游传播成缺失的类型与文档。为什么这么严一条从序列化器到 MCP 工具的类型管线SKILL.md开篇给出了一张决定 PostHog API 工程质量的核心数据流图Django serializer → drf-spectacular → OpenAPI JSON → Orval → Zod schemas → MCP tools这条链路的含义是DRF 序列化器是全仓库类型定义的唯一事实来源。每个help_text、每个字段类型、每个extend_schema注解都会一路流向下游help_text变成 OpenAPI 字段描述再变成 Zod 的.describe()字符串最终成为 LLM/Agent 决定如何填充参数时阅读的说明文本一个缺失help_text的字段意味着 Agent 只能靠猜来传参一个裸ListField()会在生成的 Zod schema 中变成z.unknown()一个缺少extend_schema的自定义action会被 drf-spectacular 发现为零参数生成的 MCP 工具拿到z.object({})。因此把序列化器写对等于同时为前端类型、MCP 工具和 API 文档交付正确的类型与描述。这条管线的背景资料见 docs/published/handbook/engineering/type-system.md。先审计后动手improving-drf-endpoints 的工作流程规则文件要求编辑前先调用该技能技能推荐的审计顺序是先看生成产物再改 Python 源码Triage——先看生成的类型文件核心 API 的生成文件在frontend/src/generated/core/产品 API 在products/product/frontend/generated/。每个产品有两份关键文件api.schemas.ts由序列化器派生的 TypeScript 接口。搜索序列化器名查找unknown类型裸ListField/JSONField的产物、缺失 JSDoc 描述缺失help_text的产物、以及过度泛化的Recordstring, unknown形状api.tsAPI 客户端函数。如果端点操作根本不存在说明 viewset 方法缺少extend_schema。按清单逐项审计对照 SKILL 中的 16 条审核清单见下文各节逐一检查字段与注解。修复并验证修改后运行hogli build:openapi重新生成 schema确认生成的 MCP 工具 schema 仍然暴露全部 OpenAPI 请求体字段尤其是改写过partial_update的request覆盖之后。技能还附有一张决策流程图SKILL.md中的digraph audit先判断是ModelViewSet有serializer_class时可被 drf-spectacular 自动发现、普通ViewSet/自定义action必须手动注解、还是 facade 产品DataclassSerializer再分别进入检查字段、添加 schema 注解、聚焦 help_text 与响应声明分支最后统一检查响应类型、分页与错误 schema。铁律一每个 viewset 方法都必须有 schema 注解validated_request首选装饰器一个搞定校验 文档规则首推validated_request它定义在 posthog/api/mixins.py把请求体校验、查询参数校验和extend_schema合并进一个装饰器并在方法体运行前自动设置request.validated_data与request.validated_query_data。完整参数签名如下def validated_request( request_serializer: type[serializers.Serializer] | None None, # 请求体序列化器 *, query_serializer: type[serializers.Serializer] | None None, # 查询参数序列化器 responses: dict[int, OpenApiResponse | None] | None None, # 状态码 - 响应 schema summary: str | None None, # OpenAPI operation summary description: str | None None, # OpenAPI operation 描述 tags: list[str] | None None, # Swagger UI 分组标签 deprecated: bool False, strict_request_validation: bool True, # 请求体校验失败是否抛错 strict_response_validation: bool False, # 响应校验是否抛错默认仅告警 include_serializer_context: bool False, # 是否注入 request/view/team 上下文 **extend_schema_kwargs, ) - Callable:核心使用模式from posthog.api.mixins import validated_request, ValidatedRequest from drf_spectacular.utils import OpenApiResponse class TaskViewSet(viewsets.ModelViewSet): validated_request( query_serializerTaskListQuerySerializer, responses{ 200: OpenApiResponse(responseTaskSerializer, descriptionList of tasks), }, summaryList tasks, descriptionGet a list of tasks for the current project, optionally filtered by repository., ) def list(self, request, *args, **kwargs): repository request.validated_query_data[repository] # 直接使用已校验的数据其实现要点对应 mixins.py装饰器栈validated_request内部先调用extend_schema(request..., parameters..., responses..., summary..., ...)把query_serializer追加进parameters因此一次装饰同时产出 OpenAPI schema随后用wraps(view_func)包裹原方法。请求校验query_serializer用request.query_params实例化并is_valid(raise_exceptionTrue)结果写入validated_query_datarequest_serializer用request.data实例化默认strict_request_validationTrue时校验失败直接抛异常若关闭 strict 且settings.DEBUG为真则记录一条请求体与声明的序列化器不匹配请更新 API schema的告警日志mixins.py。响应校验五步流程视图方法返回后装饰器按顺序检查——① 是否声明了responses② 返回值是否为Response对象③ 状态码是否在responses声明范围内④ 该状态码是否声明为None即无响应体此时响应体必须为空⑤ 用声明的响应序列化器反序列化响应数据验证匹配mixins.py。这些步骤在strict_response_validationFalse默认时只产生 DEBUG 告警不影响响应在True时抛serializers.ValidationError。上下文注入默认请求/查询序列化器以空 context 构造与 DRF 自带的get_serializer()总是注入request/view/format不同。当序列化器的validate()需要读取self.context[request]或self.context[team]时需显式传include_serializer_contextTruemixins.py。该行为有一整套测试覆盖见 posthog/api/test/test_mixins.py包括合法/缺字段的请求校验、未声明状态码的告警、响应数据不匹配告警、strict 模式下抛异常、validated_query_data可用性、204无响应体声明等二十余个用例。TypedRequest让 validated_data 拥有类型默认request.validated_data被注解为dict[str, Any]。当DataclassSerializer的validated_data返回 dataclass 实例时可用TypedRequest[T]同样定义于 mixins.py让类型检查器跟随真实形状from posthog.api.mixins import TypedRequest, validated_request class RepoViewSet(viewsets.GenericViewSet): validated_request( request_serializerCreateRepoInputSerializer, responses{201: OpenApiResponse(responseRepoSerializer, descriptionCreated repo)}, ) def create(self, request: TypedRequest[CreateRepoInput], **kwargs) - Response: data request.validated_data # 类型检查器知道这是 CreateRepoInput repo api.create_repo(data, team_idself.team_id) return Response(RepoSerializer(repo).data, statusstatus.HTTP_201_CREATED)对于普通 dict 载荷ValidatedRequest就够用。extend_schema仅需 schema 元数据时的选择当端点只需要 schema 元数据、或模式不匹配validated_request时直接使用 drf-spectacular 的extend_schemafrom drf_spectacular.utils import extend_schema, OpenApiResponse class SentimentViewSet(viewsets.ViewSet): extend_schema( requestSentimentRequestSerializer, responses{ 200: SentimentBatchResponseSerializer, 400: OpenApiResponse(descriptionInvalid request data), }, tags[LLM Analytics], summaryAnalyze sentiment, descriptionRun sentiment analysis on a batch of LLM generations., ) def create(self, request, **kwargs): serializer SentimentRequestSerializer(datarequest.data) serializer.is_valid(raise_exceptionTrue) # ...关键陷阱装饰器必须落在实际 HTTP 处理方法上而不是类或辅助函数上。extend_schema放在APIView类上无效# 错误 —— 类上装饰无效 extend_schema(requestMySerializer) class MyView(APIView): def post(self, request): ... # 正确 —— 放在处理方法上 class MyView(APIView): extend_schema(requestMySerializer, responses{201: MyResponseSerializer}) def post(self, request): ...对于继承来的方法list、create、retrieve等使用extend_schema_view统一装饰from drf_spectacular.utils import extend_schema_view, extend_schema extend_schema_view( listextend_schema(descriptionList all feature flags for the project), retrieveextend_schema(descriptionGet a single feature flag by ID), ) class FeatureFlagViewSet(viewsets.ModelViewSet): serializer_class FeatureFlagSerializer # ...自定义 action 必须有注解每个action都要显式声明 schema否则 drf-spectacular 会生成零参数MCP 工具拿到z.object({})。注意装饰器顺序extend_schema位于action之上extend_schema( requestEvaluateRequestSerializer, responses{200: EvaluateResponseSerializer}, summaryRun evaluation, descriptionExecute an evaluation run against the specified dataset., ) action(detailFalse, methods[post], url_pathevaluate) def evaluate(self, request, **kwargs): ...错误响应也要类型化OpenApiTypes.OBJECT告诉下游消费者任何关于错误结构的信息。至少使用OpenApiResponse(description...)描述语义更佳做法是定义可复用的错误序列化器class ValidationErrorSerializer(serializers.Serializer): attr serializers.CharField(help_textField that failed validation) code serializers.CharField(help_textError code) detail serializers.CharField(help_textHuman-readable error message) extend_schema( responses{ 200: MySerializer, 400: OpenApiResponse(responseValidationErrorSerializer, descriptionValidation failed — returns field-level errors), 404: OpenApiResponse(descriptionResource not found), }, )另外204 No Content的注解必须用responses{204: None}表示无响应体而不是OpenApiTypes.NONE——后者产生{schema: null}属于无效 OpenAPI会破坏 Orval 校验。分页声明自定义action默认继承父 viewset 的分页器若不期望分页应在 action 上显式关闭action(detailFalse, methods[get], pagination_classNone, filter_backends[]) def summary(self, request, **kwargs): ...请求/响应序列化器拆分当输入与输出形状不同时必须拆分。写序列化器只含可写字段读序列化器含计算字段viewset 中按 action 选择def get_serializer_class(self): if self.action in (create, update, partial_update): return ExperimentWriteSerializer return ExperimentReadSerializerdrf-spectacular 的COMPONENT_SPLIT_PATCH默认开启会为 PATCH 自动生成独立 schemaPATCH 不要求全字段必填。流式端点SSE/流式响应无法完整类型化但请求 schema 仍要声明extend_schema( requestInputSerializer, responses{(200, text/event-stream): OpenApiTypes.STR}, )x-product 归属声明ViewSet 位于products/name/backend/时通过模块路径自动归属产品而位于posthog/api/或ee/的 ViewSet 必须通过extend_schema(extensions{x-product: product})显式声明归属接受普通字符串如product_analytics或ProductKey.X枚举。不要用tags[product]影响代码生成路由——tags只用于 Swagger UI 展示。缺少x-product时MCP 脚手架与前端类型生成器无法把端点路由到正确的产品。团队嵌套端点的路由规范PostHog 曾短暂拆分过 project 与 environment 概念后又回滚因此/api/projects/:team_id/...是团队嵌套端点的唯一规范路径/api/environments/:team_id/...仅是兼容别名。新端点一律注册到routers.projects路由写在各产品自己的products/name/backend/routes.py# products/name/backend/routes.py from posthog.api.routing import RouterRegistry def register_routes(routers: RouterRegistry) - None: routers.projects.register(rmy_thing, MyThingViewSet, project_my_thing, [team_id])产品路由是自动发现的posthog/api/__init__.py遍历INSTALLED_APPS对每个带routes.py的products.*应用调用register_routes(routers)核心先注册四个父路由rootprojects/environments/organizations见 posthog/api/routing.py产品只能挂载到父路由上互不嵌套。注册刻意保持急切首次 importposthog.api时执行而非移入AppConfig.ready()——因为ready()在每个进程的django.setup()中运行若在此时注册路由会引入 viewset import把整个 API 拖入setup()破坏API 不进入 Celery worker 与管理命令的懒加载设计。兼容别名的实现是EnvironmentsRewriteMiddleware进程内把/api/environments/*路径重写到等价的/api/projects/*viewset无 307 跳转无需为 env 注册任何路由。铁律二每个 serializer 字段都必须有 help_texthelp_text 写作指南help_text的读者是 LLM/Agent而非人类文档。写作准则详见 serializer-fields.md描述用途而非类型UUID of the parent dashboard 而不是 a UUID提及格式约束ISO 8601 datetime string、comma-separated list列出合法取值One of: active, archived, deleted说明默认值Defaults to the current projects timezone明确 null/空值语义Pass null to remove the filter。# 错误 —— Agent 不知道字段期望什么 name serializers.CharField() # 正确 —— 清晰可执行的描述 name serializers.CharField( help_textHuman-readable name for the action. Used in the UI and API responses. )ListField 必须带 child裸ListField()会生成z.unknown()tags serializers.ListField( childserializers.CharField(), help_textTags to apply to this resource. Each tag is a plain string., ) steps serializers.ListField( childActionStepSerializer(), help_textOrdered list of action steps. Each step defines a match condition., )JSONField 用 extend_schema_field Pydantic裸JSONField生成泛化 object schema。标准解法是自定义字段类并指向 Pydantic 模型模式出处为products/alerts/backend/api/alert.pyfrom drf_spectacular.utils import extend_schema_field from pydantic import BaseModel class AlertCondition(BaseModel): type: str threshold: float operator: str extend_schema_field(AlertCondition) # type: ignore[arg-type] class AlertConditionField(serializers.JSONField): pass class AlertSerializer(serializers.ModelSerializer): condition AlertConditionField( requiredFalse, allow_nullTrue, help_textCondition that triggers the alert. See AlertCondition schema., )简单场景可用extend_schema_field(OpenApiTypes.OBJECT)至少告诉 Orval它是 object 而非 unknown。SerializerMethodField 注解 getter不注解时 drf-spectacular 无法推断返回类型class TeamSerializer(serializers.ModelSerializer): member_count serializers.SerializerMethodField( help_textNumber of members in this team ) extend_schema_field(serializers.IntegerField()) def get_member_count(self, obj): return obj.members.count()ChoiceField 显式 choices优先 TextChoices 类status serializers.ChoiceField( choices[active, archived, deleted], help_textCurrent status of the resource., )format、type、status、kind、level、mode、state、platform、provider这类通用字段名在多个组件上已有不同 choices极易碰撞。用models.TextChoices类承载 choices 是安全方案组件名取自类名与字段名解耦内联choices[...]会触发 drf-spectacular 自动命名如Format5eaEnum在--fail-on-warn下 CI 失败。DictField 类型化 valueproperties serializers.DictField( childserializers.CharField(), help_textKey-value pairs of event properties. Keys and values are strings., )枚举命名机制ChoicesEnumNameOverrides 与 hash 陷阱drf-spectacular 默认按字段 (value, label) 对的 hash 查ENUM_NAME_OVERRIDES命名枚举组件没有 override 时名字取自字段名 序列化器名导致新增任意同名字段都会重命名一个无关枚举。PostHog 的解法是 posthog/openapi/enum_names.py 中的ChoicesEnumNameOverridesschema 构建时遍历所有django.db.models.Choices子类按类限定名派生命名EarlyAccessFeature.Stage→EarlyAccessFeatureStageEnum嵌套部分若重复外层名会被折叠如Survey.SurveyType→SurveyTypeEnum使 schema 名跟随 choices 的定义位置永不依赖枚举池。同名/同 hash 的歧义类会被跳过并在构建期由posthog.openapi.enum_name_guard大声报错。显式ENUM_NAME_OVERRIDES配置于 posthog/settings/web.py是没有任何类可承载的 choices 集的回退例如内联列表、Pydantic literal、facade 产品的框架无关 StrEnum。hash 陷阱override 必须产出与 drf-spectacular 构建期完全一致的 hash且存在两条 hash 路径——ChoiceField模型字段drf-spectacular 注入x-spec-enum-id使用list_hash([(value, label), ...])label 来自 Choices 类override 需写模型类路径如TicketPriorityEnum: products.conversations.backend.models.constants.Priority类型注解枚举如SerializerMethodField返回类型无x-spec-enum-id回退到list_hash([(value, value), ...])override 需写内联值列表如SlackSummaryCadenceEnum: [daily, weekly, monthly]整型枚举用元组[(1, 1), (8, 8)]。格式写错则 hash 不匹配告警依旧。诊断工具python manage.py find_enum_collisions它会输出字段名、hash、枚举值、hash 路径、使用组件并给出建议的 override 格式。Facade 产品模式DataclassSerializer采用 facade 模式的产品如visual_review用DataclassSerializer包装contracts.py中的冻结 dataclassviewset-annotations.md字段类型从 dataclass 自动推导天然少类型问题关注点集中在help_textdataclass 字段本身不带它需在序列化器字段覆盖中添加validated_request已是标准模式需确认响应序列化器已声明extend_schema的 tags 与 description 仍需在 viewset 方法上设置。常见反模式速查common-anti-patterns.md 提供了完整的 before/after 对照核心条目反模式后果修复字段缺help_textAgent 猜参、Zod 无描述添加用途/格式/取值说明裸JSONField()生成泛化 objectAgent 无法构造合法输入自定义字段 extend_schema_field(PydanticModel)裸ListField()z.array(z.unknown())添加child类型化元素普通 ViewSet 手写serializer.is_valid()drf-spectacular 发现不到任何东西改用validated_requestextend_schema放类上对 APIView 无效移到实际处理方法上responses{400: OpenApiTypes.OBJECT}错误形状不可解析用OpenApiResponse(description...)或错误序列化器responses{204: OpenApiTypes.NONE}产生{schema: null}破坏 Orval用responses{204: None}SerializerMethodField无注解返回类型 unknown在get_*上extend_schema_fieldfields __all__内部字段泄漏到 API显式列出字段读写共用含计算字段的序列化器写入时校验报错拆分为读写两个序列化器真实代码示例公开端点示例posthog/api/leaked_key.py 的revoke_leaked_key是validated_request的完整示范——公开免认证端点声明request_serializerLeakedKeyReportSerializer、三个状态码的响应、summary/description并用extensions{x-product: core}声明归属方法体直接使用request.validated_data[token]。产品端点示例products/tasks/backend/presentation/views/api.py 的TaskViewSet.list展示了query_serializer的实战——从request.validated_query_data读取internal、archived、channel、hog_flow_id、basic等已校验参数再据basic切换TaskBasicSerializer/TaskSerializer输出并配合action提供搜索接口。验证与 CICI 零告警schema 构建在spectacular --fail-on-warn下运行任何枚举碰撞、未注解 action、自不一致default 不在 enum 中、required 不在 properties 中、$ref 兄弟节点都会让构建失败。相关 postprocessing hook 配置于 posthog/settings/web.py。重新生成产物hogli build:openapi及其build:openapi-schema变体用于改动后重新生成 OpenAPI schema 与前端/MCP 类型。单元测试validated_request的五步校验流程由 posthog/api/test/test_mixins.py 覆盖含 strict 与非 strict 两种模式的完整行为矩阵。枚举诊断python manage.py find_enum_collisions。小结drf-endpoints规则文件的三句话背后是 PostHog 一整套以序列化器为契约源头的工程体系validated_request把校验与文档合二为一并内置响应契约校验extend_schema兜底其余场景help_text贯穿 OpenAPI → Zod → MCP 工具 → LLM Agent 的全链路。遵循这三条铁律不仅让 API 文档永不过时更让前端类型生成、MCP 工具脚手架和 Agent 参数推断在同一个事实来源上自动对齐。更深入的模式与 before/after 对照可继续查阅 SKILL.md、serializer-fields.md、viewset-annotations.md 与 quick-reference-table.md。【免费下载链接】posthog:hedgehog: PostHog is the leading platform for building self-driving products. Our developer tools – AI observability, analytics, session replay, flags, experiments, error tracking, logs, and more – capture all the context agents need to diagnose problems, uncover opportunities, and ship fixes. Steer it all from Slack, web, desktop, or the MCP.项目地址: https://gitcode.com/GitHub_Trending/po/posthog创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。