Skip to content

feat(api): tighten Response on 3 endpoints + add as_validation_errors helper#116736

Merged
azulus merged 2 commits into
masterfrom
jeremy/tighten-response-validation-and-locals
Jun 2, 2026
Merged

feat(api): tighten Response on 3 endpoints + add as_validation_errors helper#116736
azulus merged 2 commits into
masterfrom
jeremy/tighten-response-validation-and-locals

Conversation

@azulus
Copy link
Copy Markdown
Member

@azulus azulus commented Jun 2, 2026

Tighten three more endpoint return annotations from -> Response to -> Response[T], and add a shared as_validation_errors() helper in apidocs/response_types.py for the recurring Response(serializer.errors, status=400) pattern.

Round 2 of the rollout that started with #116717. Continues the principle from that PR: no cast() calls — every tightening is unblocked by typing the source (typed local variable, explicit serializer instance, or a new union arm for validation errors). The helper consolidates a 1-liner that was about to repeat across this PR and future ones.

ValidationErrorResponse is intentionally dict[str, Any] and as_validation_errors does dict(serializer.errors) (not a value-traversal) — DRF emits flat, nested, and non-field-error shapes interchangeably, and traversing the value collapses nested dicts to keys. Caught by test_post_validation_errors on the nested ReplayDeletionJobCreateSerializer.

@github-actions github-actions Bot added the Scope: Backend Automatically applied to PRs that change backend components label Jun 2, 2026
@azulus azulus force-pushed the jeremy/tighten-response-validation-and-locals branch from afc8b40 to 68a7a42 Compare June 2, 2026 22:26
@azulus azulus changed the title feat(api): tighten Response on 3 endpoints via typed locals + validation-error union feat(api): tighten Response on 3 endpoints + add validation_errors_dict helper Jun 2, 2026
Comment thread src/sentry/replays/endpoints/project_replay_jobs_delete.py Outdated
@azulus azulus force-pushed the jeremy/tighten-response-validation-and-locals branch from 68a7a42 to 13b228c Compare June 2, 2026 22:33
… helper

Continues the Response[T] rollout (#116717 was round 1). Three more
endpoints tightened via source typing — no `cast()` calls.

Two patterns demonstrated:

1. **Typed local variable** for locally-built dicts:
   - `project_filters.py`: `results: list[ProjectFilterResponse] = []`
   - `project_replay_jobs_delete.py` POST success path:
     `response: ReplayDeletionJobDetailResponse = {"data": response_data}`

2. **Validation-error union arm** for DRF `serializer.errors` paths.
   Added a shared `ValidationErrorResponse` type alias and
   `as_validation_errors(serializer)` helper to `apidocs/response_types.py`,
   alongside the existing `DetailResponse`. The helper projects DRF's
   `ReturnDict[Any, Any]` into a plain `dict[str, list[str]]` so a
   `Response[ValidationErrorResponse]` union arm is structurally satisfied
   without `cast()`:

       def post(...) -> Response[FooResponse] | Response[ValidationErrorResponse]:
           if not serializer.is_valid():
               return Response(as_validation_errors(serializer), status=400)

   Applied to `organization_trace_item_attributes.py` and
   `project_replay_jobs_delete.py` POST.

The `project_replay_jobs_delete.py` GET endpoints are tightened to
`Response[ReplayDeletionJobListResponse]` and
`Response[ReplayDeletionJobDetailResponse]` — clean tightenings.

Verification:
- mypy: 0 errors across all touched files
- prek: clean
- `sentry django spectacular`: byte-identical OpenAPI vs master

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@azulus azulus force-pushed the jeremy/tighten-response-validation-and-locals branch from 13b228c to f2646c6 Compare June 2, 2026 22:33
@azulus azulus changed the title feat(api): tighten Response on 3 endpoints + add validation_errors_dict helper feat(api): tighten Response on 3 endpoints + add as_validation_errors helper Jun 2, 2026
@azulus azulus marked this pull request as ready for review June 2, 2026 22:40
@azulus azulus requested review from a team as code owners June 2, 2026 22:40
Copy link
Copy Markdown
Contributor

@cursor cursor Bot left a comment

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 f2646c6. Configure here.

Comment thread src/sentry/replays/endpoints/project_replay_jobs_delete.py
Comment thread src/sentry/replays/endpoints/project_replay_jobs_delete.py
…dation_errors

The `{k: list(v) for k, v in serializer.errors.items()}` transform
collapsed nested error dicts (from nested serializers or
`validate()`-raised non-field errors) to just the key list, losing the
actual error messages. `ProjectReplayDeletionJobsIndexTest::test_post_validation_errors`
caught this — `ReplayDeletionJobCreateSerializer` wraps an inner
`ReplayDeletionJobCreateDataSerializer`, so its errors are
`{"data": {"non_field_errors": [...]}}` and `list({"non_field_errors": [...]})`
returned `["non_field_errors"]` instead of preserving the message.

Fix: pass DRF's structure through verbatim — `dict(serializer.errors)`
just narrows the static type without traversing. Widen the alias to
`dict[str, Any]` to be honest about the runtime shape (flat lists,
nested dicts, non-field-error dicts all occur in real serializers).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@azulus azulus merged commit 3983e4f into master Jun 2, 2026
65 checks passed
@azulus azulus deleted the jeremy/tighten-response-validation-and-locals branch June 2, 2026 23:24
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