Django (Rest Framework): create (POST) a nested/sub-resource using hyperlinked relations
Asked Answered
M

3

6

I'm having a hard time trying to understand how the hyperlinked serializers work. If I use normal model serializers it all works fine (returning id's etc). But I'd much rather return url's, which is a bit more RESTful imo.

The example I'm working with seems pretty simple and standard. I have an API which allows an 'administrator' to create a Customer (a company in this case) on the system. The Customer has attributes "name", "accountNumber" and "billingAddress". This is stored in the Customer table in the database. The 'administrator' is also able to create a Customer Contact (a person/point of contact for the Customer/company).

The API to create a Customer is /customer. When a POST is done against this and is successful, the new Customer resource is created under /customer/{cust_id}.

Subsequently, the API for creating a Customer Contact is /customer/{cust_id}/contact. When a POST Is done against this and is successful, the new Customer Contact resource is created under /customer/{cust_id}/contact/{contact_id}.

I think this is pretty straightforward and is a good example of a Resource Oriented Architecture.

Here are my models:

class Customer(models.Model):
    name = models.CharField(max_length=50)
    account_number = models.CharField(max_length=30, name="account_number")
    billing_address = models.CharField(max_length=100, name="billing_address")

class CustomerContact(models.Model):
    first_name = models.CharField(max_length=50, name="first_name")
    last_name = models.CharField(max_length=50, name="last_name")
    email = models.CharField(max_length=30)
    customer = models.ForeignKey(Customer, related_name="customer")

So there's a foreign key (many to one) relationship between the CustomerContact and Customer.

To create a Customer is pretty simple:

class CustomerViewSet(viewsets.ViewSet):
    # /customer POST
    def create(self, request):
        cust_serializer = CustomerSerializer(data=request.data, context={'request': request})
        if cust_serializer.is_valid():
            cust_serializer.save()
            headers = dict()
            headers['Location'] = cust_serializer.data['url']
            return Response(cust_serializer.data, headers=headers, status=HTTP_201_CREATED)
        return Response(cust_serializer.errors, status=HTTP_400_BAD_REQUEST)

Creating a CustomerContact is a bit trickier as I have to get the foreign key of the Customer, add it to the request data and pass that to the serializer (I'm not sure if this is the right/best way to do it).

class CustomerContactViewSet(viewsets.ViewSet):
    # /customer/{cust_id}/contact POST
    def create(self, request, cust_id=None):
        cust_contact_data = dict(request.data)
        cust_contact_data['customer'] = cust_id
        cust_contact_serializer = CustomerContactSerializer(data=cust_contact_data, context={'request': request})
        if cust_contact_serializer.is_valid():
            cust_contact_serializer.save()
            headers = dict()
            cust_contact_id = cust_contact_serializer.data['id']
            headers['Location'] = reverse("customer-resource:customercontact-detail", args=[cust_id, cust_contact_id], request=request)
            return Response(cust_contact_serializer.data, headers=headers, status=HTTP_201_CREATED)
        return Response(cust_contact_serializer.errors, status=HTTP_400_BAD_REQUEST)

The serializer for the Customer is

class CustomerSerializer(serializers.HyperlinkedModelSerializer):
     accountNumber = serializers.CharField(source='account_number', required=True)
    billingAddress = serializers.CharField(source='billing_address', required=True)
    customerContact = serializers.SerializerMethodField(method_name='get_contact_url')

    url = serializers.HyperlinkedIdentityField(view_name='customer-resource:customer-detail')

    class Meta:
        model = Customer
        fields = ('url', 'name', 'accountNumber', 'billingAddress', 'customerContact')

    def get_contact_url(self, obj):
        return reverse("customer-resource:customercontact-list", args=[obj.id], request=self.context.get('request'))

Note (and possibly ignore) the customerContact SerializerMethodField (I return the URL for CustomerContact in the representation of the Customer resource).

The serializer for the CustomerContact is:

class CustomerContactSerializer(serializers.HyperlinkedModelSerializer):
    firstName = serializers.CharField(source='first_name', required=True)
    lastName = serializers.CharField(source='last_name', required=True)

    url = serializers.HyperlinkedIdentityField(view_name='customer-resource:customercontact-detail')

    class Meta:
        model = CustomerContact
        fields = ('url', 'firstName', 'lastName', 'email', 'customer')

'customer' is the reference to the customer foreign key in the CustomerContact model/table. So when I do a POST like so:

POST http://localhost:8000/customer/5/contact
     body: {"firstName": "a", "lastName":"b", "email":"[email protected]"}

I get back:

{
    "customer": [
        "Invalid hyperlink - No URL match."
    ]
}

So it seems that foreign key relationships have to be expressed as URL's in HyperlinkedModelSerializer? The DRF tutorial (http://www.django-rest-framework.org/tutorial/5-relationships-and-hyperlinked-apis/#hyperlinking-our-api) seems to say this too:

Relationships use HyperlinkedRelatedField, instead of PrimaryKeyRelatedField

I'm perhaps doing something wrong in my CustomerContactViewSet, is adding the customer_id to the request data before passing it to the serializer (cust_contact_data['customer'] = cust_id) incorrect? I tried passing it a URL instead - http://localhost:8000/customer/5 - from the POST example above, but I get a slightly different error:

{
    "customer": [
        "Invalid hyperlink - Incorrect URL match."
    ]
}

How do I use the HyperlinkedModelSerializer to create an entity which has a foreign key relationship with another model?

Mogerly answered 1/7, 2015 at 10:39 Comment(2)
hi @Cliff Sun I'm getting a quite similar issue, did you ever manage to get past this?Alphanumeric
hey @Cliff Sun I managed to solve the issue for me, check my answer below ;)Alphanumeric
A
1

Well, I dug a bit into the rest_framework and it seems that the mismatch is due to the URL pattern matching not resolving to your appropriate view namespace. Do some prints around here and you can see the expected_viewname not matching the self.view_name.

Check that your view namespacing is correct on your app (is seems these views are under namespace customer-resource), and if need be fix the view_name attribute on your relevant hyperlinked related fields via the extra_kwargs on the Serializer Meta:

class CustomerContactSerializer(serializers.HyperlinkedModelSerializer):
    firstName = serializers.CharField(source='first_name', required=True)
    lastName = serializers.CharField(source='last_name', required=True)

    url = serializers.HyperlinkedIdentityField()

    class Meta:
        model = CustomerContact
        fields = ('url', 'firstName', 'lastName', 'email', 'customer')
        extra_kwargs = {'view_name': 'customer-resource:customer-detail'}

Hope this works for you ;)

Alphanumeric answered 9/6, 2016 at 17:27 Comment(0)
M
0

I'm not sure to understand well, but If customer_id is a primary key as you say, I think if you specify in CustomerContactSerializer that customer is a PrimaryKeyRelatedField, you can fix your problem, something like.

class CustomerContactSerializer(serializers.HyperlinkedModelSerializer):
    firstName = serializers.CharField(source='first_name', required=True)
    lastName = serializers.CharField(source='last_name', required=True)
    customer = serializers.PrimaryKeyRelatedField(
            queryset=Customer.objects.all(),
            many=False)

    class Meta:
        model = CustomerContact
        fields = ('url', 'firstName', 'lastName', 'email', 'customer')
Madelainemadeleine answered 2/11, 2019 at 0:44 Comment(0)
U
-2

URL has to contain / at the end:

cust_contact_data['customer'] = 'http://localhost:8000/customer/5/'

This should work.

Unrefined answered 8/7, 2015 at 19:47 Comment(2)
the trailing slash did not solve this for my case. have you successfully tested this?Alphanumeric
I answered this question ~ year ago, so I do not remember now :) As I see now the problem is in cust_contact_data['customer'] = cust_id line. By default hyperlinked serializer expects to get customers URL as customer, not id.Unrefined

© 2022 - 2024 — McMap. All rights reserved.