In Django REST Framework, why does a generated OpenAPI schema often misdescribe request and response bodies, and how do you correct it?
answer
- inspected, never executed
- one serializer for both directions
- only the happy-path status
- override the request and response hooks
basics
~20 sA DRF generator infers bodies from view.get_serializer(): one serializer for request and response, one success status, fallback types for unmappable fields. Correct it by declaring separate request and response serializers, error responses and explicit types, via AutoSchema overrides or a third-party generator's decorators.
solid answer
~40 sThe generator inspects, it does not execute: DRF's built-in `AutoSchema` takes `view.get_serializer()` and uses that one serializer for both the request body and the response, marking `read_only` fields `readOnly` and `write_only` fields `writeOnly`. It documents a single success status — `201` for POST, `204` for DELETE, `200` otherwise — and falls back to `string` for fields like `SerializerMethodField`. So a view that accepts one serializer and returns another, raises errors, or has no serializer at all is misdescribed. With the built-in class I subclass `AutoSchema` and override `get_request_serializer()`, `get_response_serializer()` and `get_responses()`, and set `component_name` to avoid collisions; with drf-spectacular, which DRF recommends, I declare the same things with its decorators.
code
python · 26 linesfrom rest_framework import generics, status
from rest_framework.response import Response
from rest_framework.schemas.openapi import AutoSchema
from claims.models import ExpenseClaim
from claims.serializers import ClaimCreateSerializer, ClaimSerializer
class ClaimCreateSchema(AutoSchema):
def get_request_serializer(self, path, method):
return ClaimCreateSerializer()
def get_response_serializer(self, path, method):
return ClaimSerializer()
class ClaimCreateView(generics.CreateAPIView):
queryset = ExpenseClaim.objects.all()
serializer_class = ClaimCreateSerializer
schema = ClaimCreateSchema(tags=["claims"], operation_id_base="Claim")
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
claim = serializer.save(owner=request.user)
return Response(ClaimSerializer(claim).data, status=status.HTTP_201_CREATED)go deeper
Know that the schema is built from your serializers, so a wrong serializer on the view means a wrong schema for clients.
Explain AutoSchema's rules: get_serializer() for both directions, readOnly and writeOnly flags, one success status, string fallback, and the hooks that override each.
Diagnose a misleading schema from its symptoms, such as collided components or missing fields after a warning, and fix it at the source rather than hand-editing output.
Set the rule that every public operation declares its request, response and error shapes explicitly where inference fails, and that generation warnings fail the build.
## Where a generated body schema comes from A DRF schema generator does not run your endpoints. It **inspects** them: for each operation it asks the view for a serializer and translates the serializer's fields into a JSON-schema-like description. With DRF's built-in `AutoSchema` (the default `DEFAULT_SCHEMA_CLASS`, deprecated in 3.18 but still present), the rules are simple: - The serializer is whatever **`view.get_serializer()`** returns — normally `serializer_class`. - The **same serializer** describes the request body (for `POST`, `PUT`, `PATCH`) and the response body, stored once under `components.schemas` and referenced with `$ref`. - `read_only=True` fields are marked **`readOnly`** and `write_only=True` fields **`writeOnly`** inside that shared component. - The component's name is the serializer class name with `Serializer` removed — `ClaimSerializer` becomes `Claim`. - The response is **one success code**: `201` for `POST`, `204` with no body for `DELETE`, `200` for everything else, with an empty description. List views wrap the item in an array, or in the paginator's envelope when pagination is configured. Everything that does not fit those rules is where the schema lies. ## The usual inaccuracies | Symptom in the published schema | Cause in the code | |---|---| | The create request shows fields the client cannot send, or misses ones it must | The view reads `ClaimCreateSerializer` in `create()` but `get_serializer()` returns `ClaimSerializer` | | Only a `201` is documented; no `400`, `401`, `403` or `404` | The built-in inspector documents the success response only | | A computed field is typed `string` | `SerializerMethodField` and other unmappable fields fall back to `string` | | An operation has an empty body schema `{}` | A plain `APIView` has no `get_serializer()`, so nothing can be inspected | | Fields of one serializer appear in another endpoint's body | Two serializers with the same class name in different apps map to the same component name; the generator warns that the component was overridden with a different value | | Fields are missing entirely, with a warning at generation time | `get_serializer()` raised an `APIException` during inspection, for example from a permission check inside `get_serializer_class()` | ## Parameters and lists come from other classes Bodies are not the only inferred part. Query parameters come from the **pagination class** and each **filter backend** — the inspector calls their `get_schema_operation_parameters()` — and path parameters come from the URL pattern. For a list view the response becomes an array of the item component, and when a paginator is configured, the paginator's own response schema wraps it, for example the `count`, `next`, `previous` and `results` envelope of `PageNumberPagination`. A custom paginator or filter backend that does not implement those hooks leaves its parameters and envelope undocumented, which is another place the schema can silently fall behind the code. ## Correcting it with the built-in `AutoSchema` 1. **Separate input and output.** Subclass `AutoSchema` and override `get_request_serializer()` and `get_response_serializer()` so each returns the serializer that side really uses; attach it with `schema = ClaimSchema()` on the view. 2. **Name components deliberately.** Pass `component_name="PartnerClaim"` to `AutoSchema` when two serializers would otherwise collide, and keep serializer class names unique. 3. **Group and identify operations.** `tags=[...]` and `operation_id_base="Claim"` are constructor arguments; duplicate operation ids trigger a generation warning, and many client generators need them unique. 4. **Describe what cannot be inferred.** Error responses and non-serializer bodies need an override of `get_responses()` or `get_request_body()` in the subclass. ## Correcting it with a third-party generator The DRF documentation recommends **drf-spectacular** for new work, and the reason is precisely this list: it is built to extract more from the code and provides **decorators and extensions** to declare, per operation, the request serializer, the response serializers keyed by status code, parameters and examples. The mechanics differ, the discipline is the same: - Declare separate request and response shapes where the view uses them. - Declare error responses the client must handle, not only the success body. - Give computed fields an explicit type instead of accepting a fallback. - Treat every generator warning as a defect, since each one marks an operation the tool could not describe. ## A quick self-check for any DRF schema - Can a client build a valid request for each write operation from the schema alone? - Does each operation list the error statuses a client will actually meet? - Does every component name map to exactly one serializer? - Did generation finish without warnings?
- Why can get_serializer() raising during generation silently thin out the schema?Schema generation calls `get_serializer()` without a normal request, and with none at all when run offline. If that raises an `APIException`, such as a permission check inside `get_serializer_class()`, DRF's `AutoSchema` catches it, warns that fields will not be generated, and documents the operation without them. Other exceptions, like an `AttributeError` from reading a missing request, abort generation instead. Make serializer selection safe without a request.
- What does a component-override warning tell you?Two different serializers produced the same component name — typically two `ClaimSerializer` classes in different apps, both mapped to `Claim`. The generator keeps one and warns that the component was overridden with a different value, so one endpoint is documented with another's fields. Rename a serializer or pass `component_name` to `AutoSchema`.
saying these in an interview costs you the question
- Believing the generator calls each endpoint to learn its response shape
- Assuming validation and permission errors appear in the schema automatically
- Expecting SerializerMethodField to carry its method's return type in the built-in schema
- Ignoring generation warnings as harmless noise
- Using one serializer for input and output when the view uses two