Módulo 3 · Diseño de APIs

Lección 15 — Validación y contratos con OpenAPI

Validación de entrada y un contrato que el cliente puede leer sin preguntarte.

Publicada
En esta lección
  1. Objetivos
  2. 1. Las capas de validación (en orden)
  3. 2. Serializers honestos
  4. 3. OpenAPI con drf-spectacular
  5. 4. El contrato en CI
  6. Autoevaluación

Stack: DRF + drf-spectacular · Proyecto: TicketFlow Estado: Publicada Prerrequisito: Lección 14 — Idempotencia


Objetivos

  1. Ordenar las capas de validación: sintaxis → tipos → negocio (y qué vive en cada una).
  2. Escribir serializers que fallan con mensajes útiles (y que no filtran campos).
  3. Generar el contrato OpenAPI con drf-spectacular y mantenerlo verificado en CI.

1. Las capas de validación (en orden)

  1. Sintaxis/parsing: ¿es JSON válido? ¿Content-Type correcto? → 400 antes de tocar dominio.
  2. Forma/tipos: serializer: campos requeridos, tipos, max_length, choices, formato (email, fecha). → 400 con errors por campo.
  3. Negocio: reglas del dominio (asiento libre, evento abierto, saldo suficiente). → 409/422 con type del RFC 7807 (13).

El error de capas: validar negocio en el serializer (validate_seats que consulta la BD) puede estar bien para casos simples, pero la exclusión mutua vive en la transacción (10), no en el validador. Regla: el serializer valida forma y referencias triviales; el servicio valida estado del mundo bajo bloqueo.

2. Serializers honestos

python
class ReserveInputSerializer(serializers.Serializer):
    event = serializers.UUIDField()                      # uuid público (13)
    seats = serializers.ListField(
        child=serializers.DictField(child=serializers.CharField()),
        min_length=1, max_length=8)                      # límites explícitos

    def validate_seats(self, value):
        # forma: cada entrada sector/row/number presente
        ...

Detalles que separan al senior: min_length/max_length en listas (evita POSTs de 10.000 asientos); write_only para campos de entrada que no vuelven; read_only en uuid/created_at; mensajes de error en el idioma del negocio ("El asiento A-12 no existe en este evento", no "Invalid pk"); nunca aceptar id del cliente en creación (mass assignment: fields explícitos, jamás __all__).

3. OpenAPI con drf-spectacular

python
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")),

El contrato se genera del código (serializers + viewsets + @extend_schema para lo ambiguo). @extend_schema(request=ReserveInputSerializer, responses=ReservationOut, parameters=[...]) — donde el AutoSchema adivina, tú declaras. El contrato publicado (/api/schema/) es lo que consume el frontend (TypeScript generado) y los integradores (OpenAPI en Postman/Insomnia).

4. El contrato en CI

Contrato que no se verifica, caduca. Pipeline (Lección 41 adelantada):

  1. manage.py spectacular --file schema.yml --fail-on-warn — si un endpoint no documenta responses, falla.
  2. Diff de schema en PR: si cambió, el PR lo dice en el título (contract change review).
  3. Tests de contrato: el ejemplo del schema es válido contra la respuesta real (drf-spectacular lo audita; para E2E: schemathesis en la 35).

Autoevaluación

  1. ¿Por qué la exclusión mutua de asientos NO vive en validate_seats del serializer?
  2. ¿Qué tres atributos del serializer evitan el mass assignment y los POSTs gigantes?
  3. ¿Qué gana un integrador con /api/schema/ que no tenía leyendo tu documentación a mano?
  4. ¿Cuándo escribes @extend_schema a mano y qué pasa si nunca lo haces?
  5. ¿Qué rompe --fail-on-warn en un equipo y por qué vale la pena?

Continúa con los ejercicios. Las solutions.md solo tras intentarlo.