Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

DRF create object with nested serializers and foreign keys

I am using DRF and I'm trying to create an object which has several foreign keys as well as related objects that will need creating in the process.

Here's what a reduced version of my models looks like:

class Race(models.Model):
    name = models.CharField(max_length=200)
    owner = models.ForeignKey(User, on_delete=models.CASCADE, related_name='races')
    type = models.ForeignKey(Type, on_delete=models.SET_NULL, related_name='races', null=True)
    region = models.ForeignKey(Region, on_delete=models.CASCADE, verbose_name=_('region'), related_name='races')
    country = models.ForeignKey(Country, on_delete=models.CASCADE, related_name='races')
    timezone = models.ForeignKey(Timezone, on_delete=models.SET_NULL, null=True)

class Event(models.Model):
    name = models.CharField(max_length=200)
    race = models.ForeignKey(Race, on_delete=models.CASCADE, related_name='events')

And then here's my Race serializer:

class RaceSerializer(serializers.ModelSerializer):
    owner = UserSerializer(read_only=True)
    type = TypeSerializer(read_only=True)
    events = EventSerializer(many=True)
    country = CountrySerializer()
    region = RegionSerializer(read_only=True)
    timezone = TimezoneSerializer(read_only=True)

    def create(self, validated_data):
        with transaction.atomic():
            events = validated_data.pop('events', None)
            race = Race(**validated_data)
            race.save()
            for event in events:
                Event.objects.create(race=race, **event)
        return race

And my view:

class AddRaceView(CreateAPIView):
    serializer_class = RaceSerializer
    permission_classes = (IsAuthenticated,)

    def perform_create(self, serializer):
        serializer.save(owner=self.request.user)

And here's some test data I'm sending with my POST request:

{
    "name": "ABC Marathon",
    "country": {
        "pk": 1,
        "name": "United States"
    },
    "region": {
        "pk": 1,
        "code": "ME"
    },
    "timezone": {
        "pk": 1,
        "code": "EST"
    },
    "events": [
        {
            "name": "Marathon"
        },
        {
            "name": "Half Marathon"
        }
    ]
}

So the problem I'm having is passing valid data to the serializer for the foreign keys. I don't want to create new objects for Type, Region, Country, Timezone, only pass the appropriate data so the newly created Race object references existing foreign keys correctly.

Here's what I've tried:

1) Not setting read_only=True on the foreign key serializers. This attempts to create new objects that I don't want.

2) Setting read_only=True on the foreign key serializers (as in the code above). This helps with not attempting to create new Type, Region etc objects, but drops the respective fields from validated_data in the serializer create method. So I have no way to add the existing objects to the Race foreign keys on creation.

3) Using PrimaryKeyForeignField instead of TypeSerializer, RegionSerializer etc. But then when I use RaceSerializer to retrieve race data, I only have a pk under type, region etc and I'd really like to be able to retrieve all fields for the foreign keys.

Can you please advise on what the correct setup would look for something like this? I feel like it's shouldn't be as hard as this.

Thank you!

like image 981
Panos Avatar asked Nov 04 '25 16:11

Panos


1 Answers

So in the end I solved this by using RelatedField instead of separate serializers for every foreign key, with the exception of the nested EventSerializer that is really required to write nested Event objects.

Here's the RaceSerializer:

class RaceSerializer(serializers.ModelSerializer):
    owner = UserSerializer(read_only=True)
    type = TypeField()
    country = CountryField()
    region = RegionField()
    timezone = TimezoneField()
    events = EventSerializer(many=True)
    race_cal_types = serializers.SerializerMethodField()

    def create(self, validated_data):
        with transaction.atomic():
            events = validated_data.pop('events', None)
            race = Race(**validated_data)
            race.save()
            for event in events:
                Event.objects.create(race=race, **event)
        return race

And here is the combo of RelatedField and ModelSerializer I'm using for each field in my RaceSerializer, e.g. for the region foreign key:

class RegionSerializer(serializers.ModelSerializer):

    class Meta:
        model = Region
        fields = ('pk', 'name', 'code')


class RegionField(RelatedField):

    def get_queryset(self):
        return Region.objects.all()

    def to_internal_value(self, data):
        return self.get_queryset().get(**data)

    def to_representation(self, value):
        return RegionSerializer(value).data

Each field (type, region, country, timezone) has its own to_internal_value and to_representation methods to serializer/deserialize data the way I need to.

like image 89
Panos Avatar answered Nov 06 '25 07:11

Panos



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!