Serializers

drf-commons extends DRF’s serializer system with atomic write handling, dependency-ordered relation writes, and a comprehensive system of configurable relation field types.

BaseModelSerializer

from drf_commons.serializers import BaseModelSerializer

The base serializer for all drf-commons projects. Extends rest_framework.serializers.ModelSerializer with:

  • Atomic writes — All create() and update() operations are wrapped in transaction.atomic().

  • Relation write ordering — Fields can be designated root_first or dependency_first to control the order in which nested relation saves are applied.

Relation write ordering:

class InvoiceSerializer(BaseModelSerializer):
    # dependency_first: save the nested object first, then set FK on parent
    customer = CustomerSerializer(dependency_first=True)

    # root_first: save the parent first, then assign FK on children
    line_items = LineItemSerializer(many=True, root_first=True)

    class Meta:
        model = Invoice
        fields = ["id", "number", "customer", "line_items"]

BulkUpdateListSerializer

from drf_commons.serializers.base import BulkUpdateListSerializer

The list_serializer_class for BaseModelSerializer. Used automatically by all bulk update operations.

Behavior:

  • Validates that the incoming data list length matches the queryset length

  • Applies audit field defaults (updated_at, updated_by) when absent

  • In default mode: issues a single bulk_update() call

  • In save mode: calls instance.save() for each object within an atomic block

  • Rejects deferred nested relation writes in bulk mode (would require N+1 saves)

Configurable Field Types

The configurable field system provides a consistent, composable approach to representing foreign key and many-to-many relations in different contexts.

All field names follow the convention: {WriteFormat}To{ReadFormat}Field.

Single Relation Fields

Field Class

Write Accepts

Read Returns

Primary Use Case

IdToDataField

Primary key

Nested serializer output

Standard API pattern

IdToStrField

Primary key

String (__str__)

Lightweight references

DataToIdField

Nested data

Primary key

Accept full payload, return ID

DataToStrField

Nested data

String (__str__)

Accept nested, return label

DataToDataField

Nested data

Nested serializer output

Full nested read-write

StrToDataField

String (lookup)

Nested serializer output

Lookup by name/code

IdOnlyField

Primary key

Primary key

Pure ID relation

StrOnlyField

String (lookup)

String (__str__)

String-keyed relations

FlexibleField

ID or string (auto-detected)

Nested serializer output

Flexible client contracts

CustomOutputField

Primary key

Custom function output

Computed representations

Many-to-Many Fields

All single relation fields have a Many prefixed variant:

Field Class

Description

ManyIdToDataField

Accept list of IDs, return list of nested data

ManyDataToIdField

Accept list of nested data, return list of IDs

ManyStrToDataField

Accept list of strings, return list of nested data

ManyIdOnlyField

Accept and return list of IDs

ManyStrOnlyField

Accept and return list of strings

ManyFlexibleField

Accept mixed IDs/strings, return list of nested data

Read-Only Fields

These fields do not accept write input:

Field Class

Description

ReadOnlyIdField

Returns primary key only

ReadOnlyStrField

Returns __str__ representation

ReadOnlyDataField

Returns full nested serializer output

ReadOnlyCustomField

Returns custom function output

Import:

from drf_commons.serializers.fields import (
    IdToDataField,
    IdToStrField,
    DataToIdField,
    DataToDataField,
    StrToDataField,
    IdOnlyField,
    StrOnlyField,
    FlexibleField,
    CustomOutputField,
    ManyIdToDataField,
    ManyDataToIdField,
    ManyIdOnlyField,
    ManyFlexibleField,
    ReadOnlyIdField,
    ReadOnlyStrField,
    ReadOnlyDataField,
    ReadOnlyCustomField,
)

Field Usage Examples

Standard foreign key (write by ID, read nested):

class ArticleSerializer(BaseModelSerializer):
    author = IdToDataField(
        queryset=User.objects.all(),
        serializer=UserSerializer,
    )

Many-to-many (write list of IDs, read list of nested data):

class ArticleSerializer(BaseModelSerializer):
    tags = ManyIdToDataField(
        queryset=Tag.objects.all(),
        serializer=TagSerializer,
    )

Flexible field (accept ID or string, return nested data):

class OrderSerializer(BaseModelSerializer):
    product = FlexibleField(
        queryset=Product.objects.all(),
        serializer=ProductSerializer,
        # Tries to parse as UUID (ID); falls back to string lookup
    )

Read-only computed relation:

class UserSerializer(BaseModelSerializer):
    department = ReadOnlyDataField(serializer=DepartmentSerializer)
    role_label = ReadOnlyStrField()  # returns str(instance.role)

Custom output:

class ArticleSerializer(BaseModelSerializer):
    author = CustomOutputField(
        queryset=User.objects.all(),
        output_fn=lambda user: {"id": str(user.id), "display": user.get_full_name()},
    )

Building Custom Field Types

All field types inherit from ConfigurableRelatedFieldMixin. Implementing a custom field type requires subclassing the appropriate base and implementing the abstract transform methods:

from drf_commons.serializers.fields.base import ConfigurableRelatedField

class UrnToDataField(ConfigurableRelatedField):
    """
    Accept a URN string (e.g., "urn:product:uuid-here"), resolve to the
    object, return nested serializer data.
    """

    def to_internal_value(self, data):
        # Parse URN, extract ID, resolve object
        _, _, raw_id = data.partition(":")
        try:
            return self.get_queryset().get(pk=raw_id)
        except self.get_queryset().model.DoesNotExist:
            self.fail("does_not_exist", pk_value=raw_id)

    def to_representation(self, value):
        serializer = self.serializer_class(value, context=self.context)
        return serializer.data