ModelSerializer Composite Fields and Optional Nested ModelSerializer

Hello,

I have built a class handler that came in super handy for a starting project. I was kind of surprised seeing that Django does not look for composite fields in the serializers. As the database gets hit and the error comes from the database level. There was also the Nested ModelSerializer having options such as required or read_only, but nothing for optional. The goal of it was to introduce nested ModelSerializers if needed or not in the representation of it. This would allow a ModelSerializer to be useful in more than a situation, needing lesser serializers for edge cases. And lastly while we are at it, there’s also a context_filter that could be implemented if needed for front-end purposes from an already queried list of objects.

Here’s the code, enjoy.

DYNAMIC_METHODS_CACHE = {}


def dict_filter(representation: Dict, include: list = None, exclude: list = None):
    """Contextualize a representation so it includes/excludes fields, useful in serializers."""
    include = include or []
    exclude = exclude or []
    # Conditionally include/exclude fields based on the context
    for field in list(representation.keys()):
        if (include and field not in include) or (exclude and field in exclude):
            representation.pop(field, None)


def context_filter(representation, context):
    """Contextualize a dictionary representation to display the expected fields, useful for serializers."""
    include = context.get('include', [])
    exclude = context.get('exclude', [])

    if isinstance(representation, list):
        # If representation is a list of dictionaries, filter each one
        for item in representation:
            dict_filter(representation=item, include=include, exclude=exclude)
    else:
        # Otherwise, apply the dict_filter to the single dictionary
        dict_filter(representation=representation, include=include, exclude=exclude)


def query_filter(
        model_class: Type[models.Model],
        model_filter: Dict,
        selected_fields: List[str] = None,
        many: bool = False
):
    """Filter the queryset based on model_filter and then apply context filtering."""

    # Filtering the fields based on the model class (only valid fields will be included in filter kwargs)
    filter_kwargs = {key: value for key, value in model_filter.items() if key in [field.name for field in model_class._meta.get_fields()]}

    related_fields = []

    if selected_fields:
        for field in selected_fields:
            field_obj = model_class._meta.get_field(field)

            # Check if the field is a foreign key
            if isinstance(field_obj, ForeignKey):
                related_fields.append((field, 'select_related'))  # ForeignKey uses select_related
            # Check if the field is a many-to-many field
            elif isinstance(field_obj, ManyToManyField):
                related_fields.append((field, 'prefetch_related'))  # ManyToMany uses prefetch_related
            # Check if the field is a reverse related name (for ForeignKey on the related model)
            elif hasattr(field_obj, 'related_model'):
                related_model = field_obj.related_model
                reverse_field = field_obj.related_name  # The reverse relationship from related model
                if reverse_field:
                    related_fields.append((reverse_field, 'select_related'))

    # Query the database based on the filter and include related fields
    queryset = model_class.objects.filter(**filter_kwargs)

    # Relationships
    for field, relation_type in related_fields:
        # Apply select_related for ForeignKey or OneToOneField (single-valued relationships)
        if relation_type == 'select_related':
            queryset = queryset.select_related(field)
        # Apply prefetch_related for ManyToManyField (multi-valued relationships)
        elif relation_type == 'prefetch_related':
            queryset = queryset.prefetch_related(field)

    # Step 3: Fetch the data based on whether we want many or just one record
    if many:
        # For many records, use .values() to select specific fields if selected_fields is provided
        # Filter fields and include related fields using values_list (avoiding fields not in model)
        instances = queryset.values(*selected_fields) if selected_fields else queryset.values()
    else:
        # For a single record, fetch the first record using .first() and optionally select fields
        instance = queryset.first()

        # If selected_fields is provided, return those specific fields
        instance_data = (
            {field: getattr(instance, field) for field in selected_fields if field in [f.name for f in model_class._meta.get_fields()]}
            if selected_fields else
            {field.name: getattr(instance, field.name) for field in model_class._meta.get_fields() if not isinstance(field, models.ManyToOneRel)}
        ) if instance else None

        instances = instance_data

    return instances or None if not many else []


def add_related_fields_dynamically(serializer:Type[serializers.Serializer], related_fields:Dict[str, Type[serializers.Serializer]], selected_fields:List[str]):
    """Dynamically adds related fields to the serializer based on selected_fields."""

    # Loop through selected fields and add dynamic fields to the serializer
    for field in selected_fields:
        if field in related_fields:
            serializer.fields[field] = serializers.SerializerMethodField()
            # Dynamically define the method for this field
            def get_related_field(self, obj, field=field, serializer=related_fields[field]):
                # Dynamically serialize the related field
                related_obj = getattr(obj, field, None)
                return serializer(related_obj).data if related_obj else None

            # Add the method dynamically to the serializer
            method_name = f'get_{field}'
            setattr(serializer, method_name, get_related_field)


class CustomizedModelSerializer(serializers.ModelSerializer):
    """Model serializer class that inherits Django built-in setups with extra useful and necessary features."""
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        related_fields = getattr(self.Meta, 'related_fields', {})
        selected_fields = kwargs.get('selected_fields', [])
        # Dynamically add the methods for the related models, this is done only once through the app using cache.
        cache_key = f'{self.__class__.__name__}'
        if cache_key not in DYNAMIC_METHODS_CACHE:
            add_related_fields_dynamically(self.__class__, related_fields, selected_fields)
            DYNAMIC_METHODS_CACHE[cache_key] = True

	
    def validate(self, attrs):
	
        check_fields_unique_composite(serializer_class=self.__class__, attrs=attrs, instance=self.instance)

        return super().validate(attrs)


    def to_representation(self, instance):
	
        representation = super().to_representation(instance)

        # Filter the output of a database query with a context.
        context_filter(representation=representation, context=self.context)
        return representation

I really hope this helps someone get the things I had to go through. It seemed to me that all of this was necessary for the ModelSerializer. It does not pollute the class either, as things are largely more accessible and flexible.

Here’s an idea of how it is used:

class Model1(models.Model):
    fk_f1 = models.ForeignKey(Modle0, related_name='model0_model1s', on_delete=models.CASCADE)
    f2 = models.IntegerField(null=True)
    f3 = models.ImageField(upload_to='floor_images/', default='floor_images/default.png', blank=True)

    REQUIRED_FIELDS = []

    class Meta:
        constraints = [
            models.UniqueConstraint(fields=['fk_f1', 'f2'], name='unique_model1')
        ]

    def __str__(self):
        return f"{self.__class__.__name__} table"
		
		
class Model1Serializer(serializers.ModelSerializer):
    fk_1 = serializers.PrimaryKeyRelatedField(queryset=Land.objects.all(), required=False)
	# Not optional nested ModelSerializer; maybe adding my code so that optional would be included.
    model2 = PropertyTypeSerializer(many=False, required=False, read_only=True)
    
    class Meta:
        model = Model1
        fields = '__all__'
		related_fields = {'model2': Model2Serializer}

#services.py

		model_filter = {
            "id": model1_id
        }

        model1_list = query_filter(
            model_class=Model1,
            model_filter=model_filter,
            selected_fields=[],
            many=many,
        )

        model1_list_serializer = Model1Serializer(model1_list, selected_fields=[], context={'include':[]}, many=many)

Welcome @Xanadu333 !

Thank you for sharing your code with us.

I would like to point out, that while there are people here who will try to assist with DRF-related issues, this forum is not one of the official DRF support channels.

The Django Rest Framework is a separate project, with its own support structure. If this is a suggestion being made for it, you’re better off posting this in one of those areas.