Django Rest Framework: Writable nested serializers with Generic Foreign Key

Viewed 2415

There are examples how to create a writable nested serializer like this and then how to serialize a generic foreign key (here).

But I cannot find how to do both at the same time, i.e how to create a nested writable serializer for a generic foreign key field.

In my models there is a Meeting model with a GenericForeignKey which can be either DailyMeeting or WeeklyMeeting like:

class Meeting(models.Model):
    # More fields above
    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
    object_id = models.PositiveIntegerField()
    recurring_meeting = GenericForeignKey('content_type', 'object_id')

class DailyMeeting(models.Model):
    meeting = GenericRelation(Meeting)
    # more fields

class WeeklyMeeting(models.Model):
    meeting = GenericRelation(Meeting)
    # more fields

Then I created a custom field in my serializers.py:

class RecurringMeetingRelatedField(serializers.RelatedField):
    def to_representation(self, value):
        if isinstance(value, DailyMeeting):
            serializer = DailyMeetingSerializer(value)
        elif isinstance(value, WeeklyMeeting):
            serializer = WeeklyMeetingSerializer(value)
        else:
            raise Exception('Unexpected type of tagged object')
        return serializer.data


class MeetingSerializer(serializers.ModelSerializer):
    recurring_meeting = RecurringMeetingRelatedField()

    class Meta:
        model = Meeting
        fields = '__all__'

I am passing a JSON which looks like:

{
    "start_time": "2017-11-27T18:50:00",
    "end_time": "2017-11-27T21:30:00",
    "subject": "Test now",
    "moderators": [41],
    "recurring_meeting":{
        "interval":"daily",
        "repetitions": 10,
        "weekdays_only": "True"
        }
}

But the problem is that I am getting the following error:

AssertionError: Relational field must provide a queryset argument, override get_queryset, or set read_only=True.

Why does the Relational field has to be read_only? If I set it as read_only then it is not passed in the data in the serializer.

And what type of queryset do I have to provide?

2 Answers

You need to implement to_internal_value as well, and you can use just plain Field class.

from rest_framework.fields import Field

class RecurringMeetingRelatedField(Field):
    def to_representation(self, value):
        if isinstance(value, DailyMeeting):
            serializer = DailyMeetingSerializer(value)
        elif isinstance(value, WeeklyMeeting):
            serializer = WeeklyMeetingSerializer(value)
        else:
            raise Exception('Unexpected type of tagged object')
        return serializer.data

    def to_internal_value(self, data):
        # you need to pass some identity to figure out which serializer to use
        # supose you'll add 'meeting_type' key to your json
        meeting_type = data.pop('meeting_type')

        if meeting_type == 'daily':
            serializer = DailyMeetingSerializer(data)
        elif meeting_type == 'weekly':
            serializer = WeeklyMeetingSerializer(data)
        else:
            raise serializers.ValidationError('no meeting_type provided')

        if serializer.is_valid():
            obj = serializer.save()
        else:
            raise serializers.ValidationError(serializer.errors)

        return obj

If validation went well then you'll get created object in the MeetingSerializer validated data in other case RecurringMeetingRelatedField will raise an exception.

In this case instead of using a RecurringMeetingRelatedField in the Meeting serializer, you could define a nested serializer like this.

class RecurringMeetingSerializer(serializers.Serializer):
    interval = serializers.CharField()
    repetitions = serializers.IntegerField()
    weekdays_only = serializers.BooleanField()

    class Meta:
        fields = '__all__'


class MeetingSerializer(serializers.ModelSerializer):
    recurring_meeting = RecurringMeetingSerializer()

    class Meta:
        model = Meeting
        exclude = ['object_id', 'content_type']

    def create(self, validated_data):
        recurring_meeting = validated_data.pop('recurring_meeting')

        if recurring_meeting['interval'] == 'daily':
            instance = DailyMeeting.objects.create(**recurring_meeting)
            type = ContentType.objects.get_for_model(instance)
        else:
            instance = WeeklyMeeting.objects.create(**recurring_meeting)
            type = ContentType.objects.get_for_model(instance)

        meeting = Meeting.objects.create(content_type=type,
                                         object_id=instance.id)

        return meeting
Related