Skip to content

feat(apidocs): Lint undeclared serializers on public endpoints - #123919

Merged
azulus merged 2 commits into
masterfrom
jeremy/input-rules-1-ratchet-and-declared
Sep 9, 2026
Merged

feat(apidocs): Lint undeclared serializers on public endpoints#123919
azulus merged 2 commits into
masterfrom
jeremy/input-rules-1-ratchet-and-declared

Conversation

@azulus

@azulus azulus commented Sep 9, 2026

Copy link
Copy Markdown
Member

Adds S025, which reports a public endpoint that validates its input with one serializer while naming a different one in @extend_schema.

The schema is generated from what an endpoint declares, while the runtime accepts whatever validates, so when those disagree the endpoint accepts a shape no generated client can send. Twelve endpoints disagree today, six on the query string and six on the request body. project_repo.py is representative: it declares an inline_serializer carrying only repositoryId, then validates the body with ProjectRepoSerializer.

This is the first of three rules covering endpoint input, so it also adds ENFORCED, which names the rules whose diagnostics are fatal. It ships empty and nothing here can fail a build. That mirrors how the Response[T] linter rolled out — land the rule dark, drain the backlog, then gate — and SENTRY_INPUT_LINT_ALL=1 emits every rule regardless so a plain flake8 run over the tree produces the inventory to drain.

S025 only fires when the callee looks like a class. That skips plain calls which happen to take a data= argument, and classes chosen at runtime such as serializer_cls(data=...); the schema could not name either of those, so reporting them would be noise rather than backlog.

@github-actions github-actions Bot added the Scope: Backend Automatically applied to PRs that change backend components label Sep 9, 2026
@azulus
azulus force-pushed the jeremy/input-rules-1-ratchet-and-declared branch from 54d6ca8 to 94f77b8 Compare September 9, 2026 17:17
@azulus azulus changed the title feat(lint): Add the endpoint input ratchet and check declared serializers feat(apidocs): Lint undeclared serializers on public endpoints Sep 9, 2026
@azulus
azulus marked this pull request as ready for review September 9, 2026 17:25
@azulus
azulus requested review from a team as code owners September 9, 2026 17:25

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 94f77b8. Configure here.

Comment thread tools/flake8_plugin.py
azulus and others added 2 commits September 9, 2026 10:54
A public endpoint that validates its input with one serializer while naming a
different one in @extend_schema accepts a shape no generated client can send.
The schema is built from what is declared; the runtime accepts what validates.
Twelve endpoints currently disagree, six on the query string and six on the body.

S025 reports a serializer built from request.GET or request.data that is absent
from parameters= or request=. The callee must look like a class, which skips
plain calls that happen to take data= and classes chosen at runtime, neither of
which the schema could name.

Also adds ENFORCED, naming the rules whose diagnostics are fatal. It ships empty
so nothing here can fail a build, mirroring how the Response[T] linter rolled
out. SENTRY_INPUT_LINT_ALL=1 emits every rule regardless, so a plain flake8 run
over the tree is the backlog inventory.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@extend_schema(request=...) accepts a mapping of media type to serializer, and
two release-file endpoints declare their body that way. _declared_names only
unwrapped a list or tuple, so the serializer in a mapping counted as undeclared
and S025 would report an endpoint that had in fact declared what it validates
with.

Latent rather than live: neither endpoint currently validates with the
serializer it declares, so nothing was misreported. Fixing it now because the
rule's usefulness rests on producing no false positives.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@azulus
azulus force-pushed the jeremy/input-rules-1-ratchet-and-declared branch from 5d8f486 to 7bb98e5 Compare September 9, 2026 17:56
@sentry

sentry Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Sentry Snapshot Testing

Name Added Removed Changed Renamed Unchanged Skipped Status
sentry-frontend
sentry-frontend
- - - - - - ✅ 481 uploaded

No base snapshot found for 284f8f38eaa7bdd0c675181556bfbbf2a9acdefb. Make sure snapshots are uploaded from your main branch.

⚙️ sentry-frontend Snapshot Settings

@azulus
azulus merged commit 4a89b0b into master Sep 9, 2026
68 of 70 checks passed
@azulus
azulus deleted the jeremy/input-rules-1-ratchet-and-declared branch September 9, 2026 18:49
azulus added a commit that referenced this pull request Sep 9, 2026
…3921)

Reports a parameter read straight off `request.GET` or `request.data`
instead of through a serializer.

Such a parameter is invisible twice over. The schema has nothing to
generate from, so no client can send it, and the value arrives as an
unchecked string, so a misspelled key fails in production rather than in
review. Reading through a serializer named in `@extend_schema` fixes
both at once, because drf-spectacular derives the documented parameters
from the same field objects the handler validates with. Ten public
endpoints already work this way; this rule reports everyone else.

Three diagnostics share the rule, separating cases that differ in how
they get fixed: a literal key read straight off the input, a key
computed at runtime that no schema could ever document, and the whole
dict handed to a callable so that what it reads is unknowable from the
call site. Counting or iterating the dict is not a hand-off, building a
serializer from it is the target rather than a violation, and reads
through `validated_data` are accepted.

A read has to hang off `request` or `self.request` to count.
`serializer.data` and `response.data` are outputs a handler builds, not
parameters a client sent, and subscript writes such as
`request.data["title"] = default` are not reads either.

The rule is absent from `ENFORCED`, so nothing gates. The backlog is 197
raw reads, two computed keys, and 17 hand-offs.

Follows #123919, which added the ratchet this rule hangs off.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Scope: Backend Automatically applied to PRs that change backend components

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants