How should DRF validate polymorphic per-provider JSON payloads before delegating to a service layer?

I'm refactoring a Django/DRF system that ingests skip-trace data from multiple external bureaus (Experian, Equifax, IDI, TLO). Each provider returns a completely different JSON structure. We store the raw payload in a JSONField and normalize parts of it into relational models (DebtorContact, SkipTraceVehicle, SkipTraceProperty).

We've moved from ViewSets to explicit APIView classes plus a service layer that owns the atomic write:

# services.py
class DebtorContactService:
    @transaction.atomic
    def register_bureau_payload(self, debtor, bureau_type, raw_data):
        parsed = self._parse(bureau_type, raw_data)
        # bulk_create / bulk_update of normalized records
        return contact_record

The payload shape is determined by a bureau_type request parameter. Example (simplified):

// bureau_type = "experian"
{"consumer": {"names": [...], "tradelines": [...]}}

// bureau_type = "tlo"
{"Person": {"Vehicles": [...], "Addresses": [...]}}

A single serializer that branches on bureau_type to validate these deeply nested, structurally unrelated schemas becomes unmaintainable. Options I've tried or considered:

Option A — one endpoint per bureau (/api/skip-trace/experian/, /api/skip-trace/tlo/): clean serializers, but the view logic (auth, debtor lookup, service dispatch, response shape) is duplicated four times.

Option B — one endpoint, runtime serializer selection:

class SkipTracePayloadView(APIView):
    SERIALIZERS = {
        "experian": ExperianPayloadSerializer,
        "tlo": TLOPayloadSerializer,
        # ...
    }

    def post(self, request, debtor_id):
        serializer_cls = self.SERIALIZERS.get(request.data.get("bureau_type"))
        if serializer_cls is None:
            raise ValidationError({"bureau_type": "Unsupported bureau"})
        serializer = serializer_cls(data=request.data["payload"])
        serializer.is_valid(raise_exception=True)
        DebtorContactService().register_bureau_payload(
            debtor, request.data["bureau_type"], serializer.validated_data
        )

Option C — skip DRF validation entirely and pass raw JSON to the service layer, which validates during parsing (e.g. with dataclasses or pydantic per bureau).

My specific questions:

  1. In Option B, is there a supported DRF mechanism for this dispatch (e.g. overriding get_serializer_class on GenericAPIView based on request data), or is a manual mapping dict in post() the expected approach for APIView?

  2. If validation moves into the service layer (Option C), what should the serializer still be responsible for, given DRF's design intent that serializers own input validation? Are there concrete drawbacks (error formatting, OpenAPI schema generation, browsable API) to bypassing them for the nested payload?

Django 5.x, DRF 3.15.

bureau_type is the discriminator; keep the provider-specific payload validation at the DRF boundary, then call DebtorContactService.register_bureau_payload() with the selected serializer's validated_data.

Use a mapping in post() for APIView. For DRF's hook to choose serializers, use GenericAPIView; get_serializer_class() returns the serializer class and allows dynamic overrides based on the request context: https://www.django-rest-framework.org/api-guide/generic-views/#get_serializer_classself

from rest_framework import serializers, status
from rest_framework.response import Response
from rest_framework.views import APIView


class BureauPayloadError(Exception):
    def __init__(self, detail):
        self.detail = detail


class SkipTracePayloadView(APIView):
    SERIALIZERS = {
        "experian": ExperianPayloadSerializer,
        "tlo": TLOPayloadSerializer,
        # "equifax": EquifaxPayloadSerializer,
        # "idi": IDIPayloadSerializer,
    }

    def post(self, request, debtor_id):
        bureau_type = request.data.get("bureau_type")

        try:
            serializer_cls = self.SERIALIZERS[bureau_type]
        except KeyError:
            raise serializers.ValidationError({
                "bureau_type": ["Unsupported bureau"]
            })

        if "payload" not in request.data:
            raise serializers.ValidationError({
                "payload": ["This field is required."]
            })

        serializer = serializer_cls(
            data=request.data["payload"],
            context={"request": request},
        )
        serializer.is_valid(raise_exception=True)

        debtor = self.get_debtor(debtor_id)

        try:
            contact_record = DebtorContactService().register_bureau_payload(
                debtor=debtor,
                bureau_type=bureau_type,
                raw_data=serializer.validated_data,
            )
        except BureauPayloadError as exc:
            raise serializers.ValidationError({"payload": exc.detail})

        return Response({"id": contact_record.pk}, status=status.HTTP_201_CREATED)

If Option C moves nested validation into Pydantic/dataclasses, keep a thin DRF serializer or equivalent view checks for the envelope: bureau_type, required payload, and basic type checks. The service-layer validator must raise, or be caught and re-raised as, serializers.ValidationError with a dictionary shape such as {"payload": {"Person": {"Vehicles": ["..."]}}}; DRF handles ValidationError as a 400 Bad Request and preserves field-keyed validation responses: https://www.django-rest-framework.org/api-guide/exceptions/

The trade-off of Option C is documentation and UI introspection. With an auto-schema tool that introspects the view serializer, an opaque payload field gives the schema generator only that opaque field to document unless explicit per-provider schema documentation is added; drf-spectacular, for example, states that introspection relies heavily on the view's serializer information: https://drf-spectacular.readthedocs.io/en/latest/customization.html. The browsable API has the same practical limitation: its form can describe the envelope, not the nested Experian/TLO shapes, unless those shapes remain represented by DRF serializers or are documented separately.

My personal suggestion would be to go with 'Option A' so bureau gets its own serializer, schema, and validation logic, clean separation. As far as duplication is concerned, you can apply Factory design pattern by creating a base view (e.g. BaseSkipTraceView) that handles common logic, then extend subclasses per bureau. Each child View only overrides serializer_class and maybe a bureau_type property. That way you don’t repeat things.

Вернуться на верх