1

Question:

I need to save several objects using nested serializers using DRF.

Documentation about saving nested serializers seems to be incomplete.

Workarounds suggested in this and this answers do not seem to follow Python's encapsulation principle. As if we treat serializer as an object that encapsulates CRUD of a certain model, it is counter-intuitive to perform an object creation in serializer that "belongs" to another model.

In-detail explanation of a question:

Lets say we have two models:

class A:
    content = models.CharField()
class B:
    content = models.CharField()
    relation = models.ForeignKey('A')

And two corresponding serializers:

class BSerializer(ModelSerializer):
    class Meta:
        model = B
        fields = '__all__'
class ASerializer(ModelSerializer):
    b_field = BSerializer(many=True)
    class Meta:
        model = A
        fields = '__all__'

Sample of JSON that has to be saved:

{
    "content": "sample",
    "b_field": [
        {
            "content": "sample"
        }, 
        {
            "content": "sample"
        }
    ]
}

Using the workarounds suggested here and here, I have to override create method of ASerializer to smth like this:

class ASerializer(ModelSerializer):
    b_field = BSerializer(many=True)
    class Meta:
        model = A
        fields = '__all__'

    def create(self, validated_data):
        b_field_data = validated_data.pop('b_field', None)
        instance = super().create(validated_data)
        if b_field_data:
            for obj in b_field_data:
                B.objects.create(
                    content=obj.content,
                    relation=instance
                )
        return instance

However, as I've already pointed out above, it is kind of counter-intuitive to process class B instance creation in serializer that is responsible for class A. What is even less obvious, is that in the above ASerializer.create method, the nested BSerializer, when accessed via self.fields['b_field'] appears to be not-instantiated. Thus, I eventually need to instantiate and validate it once again in order to access BSerializer.create:

class ASerializer(ModelSerializer):
    b_field = BSerializer(many=True)
    class Meta:
        model = A
        fields = '__all__'

    def create(self, validated_data):
        b_field_data = validated_data.pop('b_field', None)
        instance = super().create(validated_data)
        if b_field_data:
            extra_instance = self.fields['b_field'](data=b_field_data)
            if extra_instance.is_valid():
                self.fields['b_field'].save(relation_id=instance.id)
                # I'm skipping further logic of BSerializer.save to simplify explanation
        return instance

I would appreciate any suggested workarounds of this issue.

Thank you.


References:

[1]: Writable nested serializers

[2]: Writable nested serializer in django-rest-framework?

[3]: Django Rest Framework writable nested serializers

1 Answer 1

0

If you do not want to manage such inside the serializer then you can move one level up, to the view and use ModelSerializer's to validate and create data related to each Model. Here is an example using ModelViewSet:

serializers.py

class BSerializer(serializers.ModelSerializer):
    class Meta:
        model = B
        fields = ["content", "relation"]
        extra_kwargs = {"relation": {"write_only": True}}


class ASerializer(serializers.ModelSerializer):
    b_field = serializers.SerializerMethodField()

    class Meta:
        model = A
        fields = ["id", "content", "b_field"]

    def get_b_field(self, obj):
        related = obj.b_set.all()
        return BSerializer(related, many=True).data

views.py

class AModelViewSet(viewsets.ModelViewSet):
    queryset = models.A.objects.all()
    serializer_class = ASerializer

    def create(self, request, *args, **kwargs):
        b_field = request.data.pop("b_field")

        # Validate and create 'A' model instance.
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        a_instance = serializer.save()

        # Add relation field and serialize 'B' models
        for obj in b_field:
            obj["relation"] = a_instance.id
            b_serializer = BSerializer(data=obj)
            if b_serializer.is_valid():
                b_serializer.save()
            else:
                a_instance.delete()
                return Response(
                    {"error": "b_field data must be valid."},
                    status=status.HTTP_400_BAD_REQUEST,
                )

        return Response(serializer.data, status=status.HTTP_201_CREATED)

Alternatively

It is also possible to pass data via extra context, although in this case you would also have to override the serializer .create method:

serializer.py

class AlternativeBSerializer(serializers.ModelSerializer):
    class Meta:
        model = B
        fields = ["content"]

    def create(self, validated_data):
        a_instance = self.context["a_model_instance"]
        return self.Meta.model.objects.create(**validated_data, relation=a_instance)


class ASerializer(serializers.ModelSerializer):
    ...

views.py

class AlternativeViewSet(viewsets.ModelViewSet):
    ...

    def create(self, request, *args, **kwargs):
        ...

        # Use 'A' instance inside the serializer
        b_serializer = AlternativeBSerializer(
            data=b_field, many=True, context={"a_model_instance": a_instance}
        )
        if b_serializer.is_valid():
            b_serializer.save()
        else:
            a_instance.delete()
            return Response(
                {"error": "b_field data must be valid."},
                status=status.HTTP_400_BAD_REQUEST,
            )

        return Response(serializer.data, status=status.HTTP_201_CREATED)
Sign up to request clarification or add additional context in comments.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.