Custom Response Content Negotiation in Django Rest Framework

Content negotiation is a crucial aspect of web APIs, allowing clients and servers to communicate about the preferred format for data exchange. In this tutorial, we’ll explore how to implement custom content negotiation in Django Rest Framework (DRF) to support additional media types beyond the default JSON and XML formats.

What is Content Negotiation?

Content negotiation involves the process of selecting the appropriate representation of a resource based on the client’s preferences and the server’s capabilities. It allows for flexible data exchange between different clients and servers that might have varying requirements for data formats. DRF provides built-in content negotiation classes, but you can also create your own to handle custom media types.

Read More about YAML, XML, JSON Response Content in Django Rest Framework

Creating a Custom Content Negotiation Class

Let’s assume we want to support two additional media types: CSV (comma-separated values) and YAML. Follow these steps to create a custom content negotiation class in DRF:

Step 1: Create a New Python File

Create a new Python file named custom_content_negotiation.py in your DRF project directory.

Step 2: Implement the Custom Content Negotiation Class

from rest_framework.negotiation import BaseContentNegotiation
from rest_framework.parsers import ParseError
from rest_framework.renderers import JSONRenderer, XMLRenderer, BrowsableAPIRenderer, TemplateHTMLRenderer
import yaml
from django.http import HttpResponse

class CustomContentNegotiation(BaseContentNegotiation):
    def select_renderer(self, request, renderers, format_suffix):
        media_type = request.accepted_media_type

        # Add your custom media types here
        custom_media_types = {
            "application/csv": "csv",
            "application/x-yaml": "yaml",
        }

        if media_type in custom_media_types:
            renderer_name = custom_media_types[media_type]
            for renderer in renderers:
                if renderer.format == renderer_name:
                    return [renderer]
        
        return super().select_renderer(request, renderers, format_suffix)

    def parse_yaml(self, stream, media_type=None, parser_context=None):
        """
        Parse the request body as YAML.
        """
        try:
            return yaml.safe_load(stream)
        except yaml.YAMLError as exc:
            raise ParseError("YAML parse error - %s" % str(exc))
        
    def parse_csv(self, stream, media_type=None, parser_context=None):
        """
        Parse the request body as CSV.
        """
        # You would need to implement your CSV parsing logic here
        raise NotImplementedError("CSV parsing is not implemented.")

    def parse_body(self, request):
        """
        Parse the request body into JSON, XML, YAML, or CSV.
        """
        if request.content_type == "application/json":
            return super().parse_body(request)
        elif request.content_type == "application/xml":
            return super().parse_body(request)
        elif request.content_type == "application/x-yaml":
            return self.parse_yaml(request.stream, media_type=request.content_type, parser_context={"request": request})
        elif request.content_type == "application/csv":
            return self.parse_csv(request.stream, media_type=request.content_type, parser_context={"request": request})
        return None

    def negotiate(self, request, view):
        """
        Perform content negotiation on the request and return a two-tuple of
        (parser, renderer) classes.
        """
        if request.method in ("POST", "PUT", "PATCH"):
            parser = self.get_parser(request)
            renderer = self.get_renderer(request)
            return parser, renderer
        return None, None

# If you want to globally apply this content negotiation class, add it to your settings:
# REST_FRAMEWORK = {
#     'DEFAULT_CONTENT_NEGOTIATION_CLASS': 'path.to.CustomContentNegotiation',
# }

Remember that you need to implement the parse_csv method with the logic to parse CSV data appropriately.

Example of CSV Response in DRF

Here’s an example of how you might implement the CSV parsing logic within the custom content negotiation class for handling the “application/csv” media type:

import csv
from rest_framework.parsers import BaseParser

class CustomContentNegotiation(BaseContentNegotiation):
    # ... (other methods)

    def parse_csv(self, stream, media_type=None, parser_context=None):
        try:
            # Parse the CSV data from the request stream
            csv_data = []
            csv_reader = csv.DictReader(stream)
            for row in csv_reader:
                csv_data.append(row)
            return csv_data
        except csv.Error as exc:
            raise ParseError("CSV parse error - %s" % str(exc))

In this example, we’ve defined a parse_csv method that takes the request stream and parses it using the csv.DictReader class. The csv.DictReader reads each row of the CSV data as a dictionary, where keys are taken from the CSV header and values from the corresponding row.

You can then call this parse_csv method within the parse_body method of the custom content negotiation class as shown in the previous example. Make sure to adjust the method calls to match your specific requirements and the structure of the CSV data you’re working with.

Step 3: Use Custom Content Negotiation

Now that your custom content negotiation class is ready, you can use it by applying it in your DRF settings.

# settings.py

REST_FRAMEWORK = {
    'DEFAULT_CONTENT_NEGOTIATION_CLASS': 'path.to.custom_content_negotiation.CustomContentNegotiation',
    # Other settings...
}

Conclusion

In this tutorial, we’ve explored the concept of content negotiation in Django Rest Framework and learned how to implement a custom content negotiation class to support additional media types such as CSV and YAML. By understanding the negotiation process and creating a custom class, you can enhance the flexibility and usebility of your web APIs.

Blogs You Might Like to Read!