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?
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 ;)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With