Search code examples
python-3.xdjangodjango-modelsdjango-rest-frameworkdjango-serializer

In Django, how do I create a serializer that will auto-generate a primary key for a member of my model?


I'm using Django 3, the Django REST framework, and Python 3.7. I have the following models. Notice that the second is dependent upon the first ...

class ContactMethod(models.Model):
    class ContactTypes(models.TextChoices):
        EMAIL = 'EMAIL', _('Email')
        PHONE = 'PHONE', _('Phone')

    type = models.CharField(
        null=False,
        max_length=5,
        choices=ContactTypes.choices,
    )
    phone = PhoneNumberField(null=True)
    email = models.EmailField(null=True)

    class Meta:
        unique_together = ('phone', 'email',)

...

class Coop(models.Model):
    objects = CoopManager()
    name = models.CharField(max_length=250, null=False)
    types = models.ManyToManyField(CoopType)
    addresses = models.ManyToManyField(Address)
    enabled = models.BooleanField(default=True, null=False)
    phone = models.ForeignKey(ContactMethod, on_delete=models.CASCADE, null=True, related_name='contact_phone')
    email = models.ForeignKey(ContactMethod, on_delete=models.CASCADE, null=True, related_name='contact_email')
    web_site = models.TextField()

I would like to submit some JSON to create my model, so I created the below two serializers to help me ...

class ContactMethodSerializer(serializers.ModelSerializer):

    class Meta:
        model = ContactMethod
        fields = ['type', 'phone', 'email']

    def create(self, validated_data):
        contact_method = ContactMethod.objects.create(**validated_data)
        return contact_method

    def to_internal_value(self, data):
        if type(data) == dict:
            contatcmethod, created = CoopType.objects.create(**data)
            # Replace the dict with the ID of the newly obtained object
            data = contactmethod.pk
        return super().to_internal_value(data)
...
class CoopSerializer(serializers.ModelSerializer):
    types = CoopTypeSerializer(many=True)
    addresses = AddressTypeField(many=True)

    class Meta:
        model = Coop
        fields = ['id', 'name', 'types', 'addresses', 'phone', 'enabled', 'email', 'web_site']

    def to_representation(self, instance):
        rep = super().to_representation(instance)
        rep['types'] = CoopTypeSerializer(instance.types.all(), many=True).data
        rep['addresses'] = AddressSerializer(instance.addresses.all(), many=True).data
        return rep

    def create(self, validated_data):
        #"""
        #Create and return a new `Snippet` instance, given the validated data.
        #"""

        coop_types = validated_data.pop('types', {})
        instance = super().create(validated_data)
        for item in coop_types:
            coop_type, _ = CoopType.objects.get_or_create(name=item['name'])  
            instance.types.add(coop_type)

        return instance

The issue is, I'm not sure how to create my phone and email contact fields without submitting a primary key (I'd like to have that auto-generated). I created this test to try ...

def test_coop_create(self):
    """ Test coop serizlizer model """
    name = "Test 8899"
    coop_type_name = "Library"
    street = "222 W. Merchandise Mart Plaza, Suite 1212"
    city = "Chicago"
    postal_code = "60654"
    enabled = True
    postal_code = "60654"
    email = EmailContactMethodFactory()
    phone = PhoneContactMethodFactory()
    web_site = "http://www.1871.com"
    state = StateFactory()
    serializer_data = {
        "name": name,
        "types": [
            {"name": coop_type_name}
        ],
        "addresses": [{
            "formatted": street,
            "locality": {
                "name": city,
                "postal_code": postal_code,
                "state_id": state.id
            }
        }],
        "enabled": enabled,
        "phone": {
          "phone": phone
        },
        "email": {
          "email": email
        },
        "web_site": web_site
    }

    serializer = CoopSerializer(data=serializer_data)
    serializer.is_valid()
    assert serializer.is_valid(), serializer.errors
    coop = serializer.save()
    assert coop.name == name
    type_count = 0
    for coop_type in coop.types.all():
        assert coop_type.name == coop_type_name
        type_count = type_count + 1
    assert type_count == 1
    assert coop.addresses.first().locality.name == city
    assert coop.addresses.first().locality.postal_code == postal_code
    assert coop.addresses.first().locality.state.id == state.id
    assert coop.enabled == enabled
    assert coop.phone.phone == phone
    assert coop.email.email == email
    assert coop.web_site == web_site

but it results in the error below

AssertionError: {'phone': [ErrorDetail(string='Incorrect type. Expected pk value, received dict.', code='incorrect_type')], 'email': [ErrorDetail(string='Incorrect type. Expected pk value, received dict.', code='incorrect_type')]}

What's the correct way to set up my serializer to create the foreign keys without having to specify an ID?

Edit: GitHub repo:

https://github.com/chicommons/maps/tree/master/web

Solution

  • Out of box, rest-framework serializes relationships (phone, email) with primary keys.

    Any relationships such as foreign keys on the model will be mapped to PrimaryKeyRelatedField. Reverse relationships are not included by default unless explicitly included as specified in the serializer relations documentation.

    source: https://www.django-rest-framework.org/api-guide/serializers/#modelserializer

    You would normally create ContactMethod objects, one for name (for instance with id=1) and second for email (id=2), before creating Coop and then include their id's in our payload. So it would look like

    {
        // ...
        "phone": 1, 
        "email": 2,
        // ...
    }
    

    In your case you need to create ContactMethod when creating Coop. You need to change CoopSerializer to accept payload serialized by ContactMethodEmailSerializer in email field and ContactMethodPhoneSerializer in phone field.

    class ContactMethodPhoneSerializer(serializers.ModelSerializer):
        class Meta:
            model = ContactMethod
            fields = ['type', 'phone']
            read_only_fields = ['type']
            extra_kwargs = {'type': {'default': 'PHONE'}}
    
    
    class ContactMethodEmailSerializer(serializers.ModelSerializer):
        class Meta:
            model = ContactMethod
            fields = ['type', 'email']
            read_only_fields = ['type']
            extra_kwargs = {'type': {'default': 'EMAIL'}}
    
    
    class CoopSerializer(serializers.ModelSerializer):
        types = CoopTypeSerializer(many=True)
        addresses = AddressTypeField(many=True)
        phone = ContactMethodPhoneSerializer()
        email = ContactMethodEmailSerializer()
    

    With this serializers your test payload should be accepted.

    In CoopSerializer.create method you need to handle creation of ContactMethod, similar to what you have done with CoopType, you can follow example in docs: https://www.django-rest-framework.org/api-guide/serializers/#writable-nested-representations