Search code examples
pythondjangodjango-rest-frameworkdjango-serializer

Best architecture for dynamically validating and saving field


I am looking for the good architecture for my problem. I am using django rest framework for building an API. I receive a list of dict which contains an id and a list of values. The list of values need to be validated according to the id.

Example of my code:

class AttributesSerializer(serializers.Serializer):
    id = serializers.PrimaryKeyRelatedField(queryset=Attribute.objects.all(), source="attribute", required=True)
    values = serializers.ListField()
    
    def validate(self, validated_data):
        attribute = validated_data["attribute"]
        values = validated_data["values"]
        
        # This function returns the corresponding field according to attribute
        values_child_field = get_values_field(attribute)
        self.fields["values"].child = values_child_fields
        new_values = self.fields["values"].run_child_validation(values)
        
        set_value(validated_data, "values", new_values)

        return validated_data


class BaseObjectApiInputSerializer(serializers.Serializer):
    category_id = serializers.PrimaryKeyRelatedField(
        queryset=Category.objects.all()
    )
    attributes = AttributesSerializer(many=True)

I want to parse json like this:

{
    "categorty_id": 42,  # Category pk of the baseobject. which defines some constraints about attributes available
    "attributes": [
        {"id": 124, "values": ["value"]},
        {"id": 321, "values": [42]},
        {
             "id": 18,
             "values": [
                 {
                     "location": {"type": "Point", "geometry": {...}},
                     "address": "an address",
                 }
             ],
        },
    ]
}

Currently, this code does not work. DRF seems to try to revalidate all values entries for each iteration with each child field. I do not understand why... I guess I could make it work without using this fields["values"] for making the validation and just retrieve the field and use it directly, but i need this field for making the save later.

Do you think my architecture is ok? What is the good way for parsing this type of data with DRF?

EDIT:

Structure of models are complex but a version simplified following:

class Attribute(models.Model):

    class DataType(models.TextChoices):
        TEXT = "TEXT", _("datatype_text")
        INTEGER = "INTEGER", _("datatype_integer")
        DATETIME = "DATETIME", _("datatype_datetime")
        BOOL = "BOOL", _("datatype_bool")
        # Some examples, but there are about 30 items with 
        # type very complicated like RecurrenceRule (RFC2445) 
        # or GeoJSON type


    label = models.CharField()
    category = models.ForeignKey(Category)
    attribute_type = models.CharField(choices=DataType.choices)


class AttributeValue(models.Model):
    attribute = models.ForeignKey(Attribute)
    # a model which represents an object with list of attributes
    baseobject = models.ForeignKey(BaseObject)
    value = models.TextField()

AttributeValue is like a through table for manytomany relation between BaseObject model and Attribute model. My JSON represents the list of attribute/values attached to a baseobject.

In fact I don't understand why DRf doesn't allow delegating registration in the child serializers of the parent serializer. This would allow much greater flexibility in code architecture and separation of responsibilities.

EDIT 2 : My urls.py

router = routers.DefaultRouter()
router.register("baseobjects", BaseObjectViewSet, basename="baseobjects")

I am using the default router and url for DRF viewset. The view looks like:

class BaseObjectViewSet(viewsets.ModelViewSet):
    permission_classes = [IsAuthenticated]
    authentication_classes = [TokenAuthentication]

    def create(self, request, *args, **kwargs):
        serializer = BaseObjectApiInputSerializer(
            data=request.data
        )
        if not serializer.is_valid():
            return Response(serializer.errors, status=HTTP_400_BAD_REQUEST)
        baseobject: BaseObject = serializer.save()
        return Response(
            {"results": [{"id": baseobject.pk}]}, status=HTTP_200_OK
        )

Solution

  • I think you should use ListField with JSONField as child argument for values field.

    validators = {
        TinyurlShortener.DataType.TEXT: serializers.CharField(),
        TinyurlShortener.DataType.INTEGER: serializers.IntegerField(),
        TinyurlShortener.DataType.DATETIME: serializers.DateTimeField(),
        TinyurlShortener.DataType.BOOL: serializers.BooleanField(),
    }
    
    class AttributesSerializer(serializers.Serializer):
        id = serializers.PrimaryKeyRelatedField(queryset=Attribute.objects.all(), source="attribute", required=True)
        values = serializers.ListField(
            child=serializers.JSONField()
        )
    
        def validate(self, attrs):
            attribute = attrs.get('id')
            field = validators[attribute.attribute_type]
            for v in attrs['values']:
                field.run_validation(json.loads(v.replace("'", '"'))) 
            
            return super().validate(attrs)
    
    
    class BaseObjectApiInputSerializer(serializers.Serializer):
        category_id = serializers.PrimaryKeyRelatedField(
            queryset=Category.objects.all()
        )
        attributes = AttributesSerializer(many=True)