python-3.xdjango-rest-frameworkhyperlinkhateoasdrf-nested-routers

How to include Hyperlinks for nested resources in drf-nested-routers to apply HATEOAS principle?


Context

I have an API in Django REST framework with the following nested resources

/wizard-api/industries/
/wizard-api/industries/<pk>/
/wizard-api/industries/<industry_pk>/sub-industries/
/wizard-api/industries/<industry_pk>/sub-industries/<pk>/
/wizard-api/industries/<industry_pk>/sub-industries/<sub_industry_pk>/details/
/wizard-api/industries/<industry_pk>/sub-industries/<sub_industry_pk>/details/<pk>/

# basenames:
wizard-api:industries-list
wizard-api:industries-detail
wizard-api:sub-industries-list
wizard-api:sub-industries-detail
wizard-api:details-list
wizard-api:details-detail

Here my URLs config using drf-nested-routers:

# Nested Routes
first_level = routers.SimpleRouter()
first_level.register(r'industries', views.IndustryViewSet, basename='industries')

second_level = routers.NestedSimpleRouter(first_level, r'industries', lookup='industry')
second_level.register(r'sub-industries', views.SubIndustryViewSet, basename='sub-industries')

third_level = routers.NestedSimpleRouter(second_level, r'sub-industries', lookup='sub_industry')
third_level.register(r'details', views.SubIndustryDetailsViewSet, basename='abc')

ERD

enter image description here

I want to apply the HATEOAS principle

# endpoint: /wizard-api/industries/1/
# response:
{
    "id": 1,
    "name": "food and beverage",
    "subindustries": "http://127.0.0.1:8000/wizard-api/industries/1/sub-industries/"
}

I made it for the first level using the HyperlinkedIdentityField in the first Serializer

class IndustryModelSerializer(serializers.ModelSerializer):

    subindustries = serializers.HyperlinkedIdentityField(
        view_name='wizard-api:sub-industries-list',
        lookup_url_kwarg='industry_pk'
    )

    class Meta:
        model = Industry
        exclude = ['created', 'modified', 'active']

Problem

The problem appears when I try to apply the same logic in the subsequent levels, i.e: generating the url from the sub-industries level to the details level:

/wizard-api/industries/<industry_pk>/sub-industries/<sub_industry_pk>/details/

I tried with the details field in the serializer of the second level (sub-industries):

class SubIndustryModelSerializer(serializers.ModelSerializer):

    details = serializers.HyperlinkedIdentityField(
        view_name='wizard-api:details-list',
        lookup_url_kwarg='industry_pk'
    )

    class Meta:
        model = SubIndustry
        exclude = ['created', 'modified', 'active']

Expected

The expected response is:

# endpoint: /wizard-api/industries/1/sub-industries/
# response:
[
    {
        "id": 1,
        "name": "beverage industries",
        "details": "http://127.0.0.1:8000/wizard-api/industries/1/sub-industries/1/details/"
    },
    {
        "id": 2,
        "name": "food production",
        "details": "http://127.0.0.1:8000/wizard-api/industries/1/sub-industries/2/details/"
    }
]

Error

But I got the following error:

Could not resolve URL for hyperlinked relationship using view name "wizard-api:details-list". You may have failed to include the related model in your API, or incorrectly configured the lookup_field attribute on this field.


Solution

  • I made some tests and found the solution.

    First, the best way to implement the nested routes in your case will be with these HyperlinkedModelSerializer, NestedHyperlinkedModelSerializer.

    So the first level should look like this:

    class IndustryModelSerializer(HyperlinkedModelSerializer):
    
        subindustries = serializers.HyperlinkedIdentityField(
            view_name='wizard-api:sub-industries-list',
            lookup_url_kwarg='industry_pk'
        )
    
        class Meta:
            model = Industry
            fields = ['subindustries', 'foo'...]
    

    The main problem in the second level can be fixed like this:

    class SubIndustryModelSerializer(NestedHyperlinkedModelSerializer):
    
    
        parent_lookup_kwargs = {
            'industry_pk': 'industry_id'
        }
    
    
        class Meta:
            model = SubIndustry
            fields = ['foo', 'url', ...]
            extra_kwargs = {
                'url': {
                    'view_name': 'wizard-api:details-list',
                    'lookup_url_kwarg': 'sub_industry_pk'
                }
            }
    

    Based on your ERD, you must include a parent_kwargs for the proper configuration of nested routes, change the exclude for fields, and add the url parameter.

    Hope it works!