Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

DRF: Simple foreign key assignment with nested serializers?

With Django REST Framework, a standard ModelSerializer will allow ForeignKey model relationships to be assigned or changed by POSTing an ID as an Integer.

What's the simplest way to get this behavior out of a nested serializer?

Note, I am only talking about assigning existing database objects, not nested creation.

I have hacked away around this in the past with additional 'id' fields in the serializer and with custom create and update methods, but this is such a seemingly simple and frequent issue for me that I'm curious to know the best way.

class Child(models.Model):
    name = CharField(max_length=20)

class Parent(models.Model):
    name = CharField(max_length=20)
    phone_number = models.ForeignKey(PhoneNumber)
    child = models.ForeignKey(Child)

class ChildSerializer(ModelSerializer):
    class Meta:
        model = Child

class ParentSerializer(ModelSerializer):
    # phone_number relation is automatic and will accept ID integers
    children = ChildSerializer() # this one will not

    class Meta:
        model = Parent
like image 307
John Rork Avatar asked Apr 29 '15 17:04

John Rork


3 Answers

Updated on July 05 2020

This post is getting more attention and it indicates more people have a similar situation. So I decided to add a generic way to handle this problem. This generic way is best suitable for you if you have more serializers that need to change to this format

Since DRF doesn't provide this functionality out of the box, we need to create a serializer field first.

from rest_framework import serializers


class RelatedFieldAlternative(serializers.PrimaryKeyRelatedField):
    def __init__(self, **kwargs):
        self.serializer = kwargs.pop('serializer', None)
        if self.serializer is not None and not issubclass(self.serializer, serializers.Serializer):
            raise TypeError('"serializer" is not a valid serializer class')

        super().__init__(**kwargs)

    def use_pk_only_optimization(self):
        return False if self.serializer else True

    def to_representation(self, instance):
        if self.serializer:
            return self.serializer(instance, context=self.context).data
        return super().to_representation(instance)

I am not well impressed with this class name, RelatedFieldAlternative, you can use anything you want. Then use this new serializer field in your parent serializer as,

class ParentSerializer(ModelSerializer):
   child = RelatedFieldAlternative(queryset=Child.objects.all(), serializer=ChildSerializer)

    class Meta:
        model = Parent
        fields = '__all__'

Original Post

Using two different fields would be ok (as @Kevin Brown and @joslarson mentioned), but I think it's not perfect (to me). Because getting data from one key (child) and sending data to another key (child_id) might be a little bit ambiguous for front-end developers. (no offense at all)


So, what I suggest here is, override the to_representation() method of ParentSerializer will do the job.

def to_representation(self, instance):
    response = super().to_representation(instance)
    response['child'] = ChildSerializer(instance.child).data
    return response


Complete representation of Serializer

class ChildSerializer(ModelSerializer):
    class Meta:
        model = Child
        fields = '__all__'


class ParentSerializer(ModelSerializer):
    class Meta:
        model = Parent
        fields = '__all__'

    def to_representation(self, instance):
        response = super().to_representation(instance)
        response['child'] = ChildSerializer(instance.child).data
        return response



Advantage of this method?

By using this method, we don't need two separate fields for creation and reading. Here both creation and reading can be done by using child key.


Sample payload to create parent instance

{
        "name": "TestPOSTMAN_name",
        "phone_number": 1,
        "child": 1
    }



Screenshot
POSTMAN screenshot

like image 186
JPG Avatar answered Nov 01 '22 09:11

JPG


The best solution here is to use two different fields: one for reading and the other for writing. Without doing some heavy lifting, it is difficult to get what you are looking for in a single field.

The read-only field would be your nested serializer (ChildSerializer in this case) and it will allow you to get the same nested representation that you are expecting. Most people define this as just child, because they already have their front-end written by this point and changing it would cause problems.

The write-only field would be a PrimaryKeyRelatedField, which is what you would typically use for assigning objects based on their primary key. This does not have to be write-only, especially if you are trying to go for symmetry between what is received and what is sent, but it sounds like that might suit you best. This field should have a source set to the foreign key field (child in this example) so it assigns it properly on creation and updating.


This has been brought up on the discussion group a few times, and I think this is still the best solution. Thanks to Sven Maurer for pointing it out.

like image 25
Kevin Brown-Silva Avatar answered Nov 01 '22 08:11

Kevin Brown-Silva


Here's an example of what Kevin's answer is talking about, if you want to take that approach and use 2 separate fields.

In your models.py...

class Child(models.Model):
    name = CharField(max_length=20)

class Parent(models.Model):
    name = CharField(max_length=20)
    phone_number = models.ForeignKey(PhoneNumber)
    child = models.ForeignKey(Child)

then serializers.py...

class ChildSerializer(ModelSerializer):
    class Meta:
        model = Child

class ParentSerializer(ModelSerializer):
    # if child is required
    child = ChildSerializer(read_only=True) 
    # if child is a required field and you want write to child properties through parent
    # child = ChildSerializer(required=False)
    # otherwise the following should work (untested)
    # child = ChildSerializer() 

    child_id = serializers.PrimaryKeyRelatedField(
        queryset=Child.objects.all(), source='child', write_only=True)

    class Meta:
        model = Parent

Setting source=child lets child_id act as child would by default had it not be overridden (our desired behavior). write_only=True makes child_id available to write to, but keeps it from showing up in the response since the id already shows up in the ChildSerializer.

like image 59
joslarson Avatar answered Nov 01 '22 08:11

joslarson