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)