Stack: DRF + drf-spectacular · Project: TicketFlow Status: Published Prerequisite: Lesson 14 — Idempotency
Objectives
- Order the validation layers: syntax → types → business (and what lives in each).
- Write serializers that fail with useful messages (and don't leak fields).
- Generate the OpenAPI contract with drf-spectacular and keep it verified in CI.
1. The validation layers (in order)
- Syntax/parsing: is it valid JSON? Correct Content-Type? → 400 before touching the domain.
- Shape/types: serializer: required fields, types, max_length, choices, formats (email, date). → 400 with per-field
errors. - Business: domain rules (seat free, event open, enough credit). → 409/422 with Lesson 13's RFC 7807
type.
The layering mistake: validating business in the serializer (a validate_seats that queries the DB) can be fine for simple cases, but mutual exclusion lives in the transaction (10), not in the validator. Rule: the serializer validates shape and trivial references; the service validates the world's state under a lock.
2. Honest serializers
class ReserveInputSerializer(serializers.Serializer):
event = serializers.UUIDField() # public uuid (13)
seats = serializers.ListField(
child=serializers.DictField(child=serializers.CharField()),
min_length=1, max_length=8) # explicit limits
def validate_seats(self, value):
# shape: each entry has sector/row/number
...Details that separate the senior: min_length/max_length on lists (no 10,000-seat POSTs); write_only for input fields that never come back; read_only on uuid/created_at; error messages in the business's language ("Seat A-12 does not exist in this event", not "Invalid pk"); never accept a client-sent id on creation (mass assignment: explicit fields, never __all__).
3. OpenAPI with drf-spectacular
INSTALLED_APPS += ["drf_spectacular"]
REST_FRAMEWORK["DEFAULT_SCHEMA_CLASS"] = "drf_spectacular.openapi.AutoSchema"
# urls.py
path("api/schema/", SpectacularAPIView.as_view(), name="schema"),
path("api/docs/", SpectacularSwaggerView.as_view(url_name="schema")),The contract is generated from code (serializers + viewsets + @extend_schema for the ambiguous parts). @extend_schema(request=ReserveInputSerializer, responses=ReservationOut, parameters=[...]) — wherever the AutoSchema guesses, you declare. The published contract (/api/schema/) is what the frontend consumes (generated TypeScript) and integrators (OpenAPI in Postman/Insomnia).
4. The contract in CI
A contract that isn't verified expires. Pipeline (Lesson 41 previewed):
manage.py spectacular --file schema.yml --fail-on-warn— if an endpoint doesn't document its responses, it fails.- Schema diff in PRs: if it changed, the PR says so in its title (contract change review).
- Contract tests: the schema's example is valid against the real response (drf-spectacular audits it; for E2E: schemathesis in Lesson 35).
Self-assessment
- Why does seat mutual exclusion NOT live in the serializer's
validate_seats? - Which three serializer attributes prevent mass assignment and giant POSTs?
- What does an integrator gain from
/api/schema/that they didn't have reading your hand-written docs? - When do you hand-write
@extend_schema, and what happens if you never do? - What does
--fail-on-warnbreak in a team, and why is it worth it?
Continue with the exercises. The solutions only after trying it yourself.