Skip to content

alaouimehdi1995/django-rest

Repository files navigation

django-REST

PyPI version PyPI pyversions PyPI license PyPI status

Build Status codecov Known Vulnerabilities Maintainability

Overview

django-REST is a tiny, lightweight, easy-to-use and incredibly fast library to implement REST views with django. The whole library's focused in one decorator that transforms the simple views into REST ones, allowing easy customizations (such as permissions, serializers, etc.)

The library itself was highly inspired from the great django-rest-framework and SerPy

Table of contents

  1. Overview

  2. Table of contents

  3. Requirements

  4. Installation

  5. Example

  6. Documentation

    1. The @api_view decorator
      1. Decorator argments
      2. Decorated view's arguments
      3. How to decorate a view
    2. View Permissions
      1. Introduction
      2. Available Permissions
      3. Permissions Operators
      4. Implement your own permission
    3. Deserializers
      1. Introduction
      2. Impmement a new Deserializer
      3. Available Deserializer Fields
      4. Nested Deserializers
      5. Post-clean methods
      6. All-pass Deserializer
    4. Serializers
      1. Introduction
      2. Impmement a new Serializer
      3. Available Serializer Fields
        1. Primitive types
        2. MethodField
        3. ConstantField
        4. ListField
      4. Nested Serializers
      5. DictSerializer
    5. Exceptions
      1. @api_view exceptions catching
      2. Existing API Exceptions
      3. Define your own API Exception
    6. HTTP
      1. HTTP Status codes
      2. HTTP Methods

Requirements

django-REST library requires:

  • Python version 2.7+ or 3.3+
  • django version 1.10+

Installation

You can get the package using pip, as the following:

pip install django-rest

Example

Let's implement a quick public API endpoint that lists existing regular (i.e. not staff) users:

First, start a new django project:

pip install django-rest # Will install django if not already installed
django-admin startproject first_project .
./manage.py migrate
./manage.py createsuperuser
# Follow instructions

Let's get started by implementing the views in ./first_project/urls.py:

from typing import Dict

from django.contrib import admin
from django.contrib.auth.models import User
from django.http import JsonResponse
from django.urls import path

from django_rest.decorators import api_view
from django_rest.http import status
from django_rest.serializers import fields as fields, Serializer


# The serializer defines the output format of our endpoints
class UserSerializer(Serializer):
    id = fields.IntegerField()
    username = fields.CharField()
    email = fields.CharField()
    is_staff = fields.BooleanField()


@api_view(allowed_methods=["GET"])
def list_users_view(request, url_params: Dict, query_params: Dict, **kwargs):
    regular_users = User.objects.exclude(is_staff=True)
    return JsonResponse(
        UserSerializer(regular_users, many=True).data,
        status=status.HTTP_200_OK,
        safe=False,
    )


urlpatterns = [
    path("admin/", admin.site.urls),
    path("api/users/", list_users_view),
]

That's all! Now run the server:

./manage.py runserver

In order to test your endpoints, you can use PostMan, httpie or curl. I'll be using httpie in the example:

http GET http://127.0.0.1:8000/api/users/

HTTP/1.1 200 OK
Content-Length: 84
Content-Type: application/json
Date: Sat, 30 May 2020 02:13:49 GMT
Server: WSGIServer/0.2 CPython/3.6.9
X-Content-Type-Options: nosniff
X-Frame-Options: DENY

[]

# After creating a new user from django-admin section (visit: http://127.0.0.1:8000/admin/ using your browser)

http GET http://127.0.0.1:8000/api/users/

HTTP/1.1 200 OK
Content-Length: 84
Content-Type: application/json
Date: Sat, 30 May 2020 02:17:23 GMT
Server: WSGIServer/0.2 CPython/3.6.9
X-Content-Type-Options: nosniff
X-Frame-Options: DENY

[
    {
        "email": "[email protected]",
        "id": 1,
        "is_staff": true,
        "username": "firstuser"
    }
]

Documentation

1. The @api_view decorator

1.1 Decorator arguments

As shown in the example section, the @api_view could be used with multiple (optional) arguments:

api_view(
    permission_class: BasePermission = AllowAny,
    allowed_methods: Iterable[str] = ALL_HTTP_METHODS,
    deserializer_class: Union[
        Deserializer, Dict[str, Deserializer]
    ] = AllPassDeserializer,
    allow_forms: bool = False,
)
  1. permission_class

    A class that defines who is allowed to access the decorated view. If no permission_class given, the decorator's default permission is AllowAny (your view is public).

    In case the user isn't allowed to access the view, a 403 Forbidden access response will be returned before even executing the view's code. More details in permissions section.

  2. allowed_methods:

    A list/tuple of HTTP allowed methods. Allowed methods should be in uppercase strings (ex.GET, POST, etc.). You can also use some predefined sets in django_rest.http.methods. If no allowed_methods given, all HTTP methods will be allowed.

    If the user requests the decorated view with a non-allowed method, a 405 Method not allowed response will be returned before executing your view's code.

  3. deserializer_class:

    Could be either a sub-class of Deserializer (as shown in the previous example), or a dict that maps HTTP methods that use payload (i.e. POST, PUT and PATCH) to Deserializer sub-classes, as the following:

    @api_view(deserializer_class=MyDeserializerClass)
    def first_case_view(request, **kwargs):
       # [...]
    
    @api_view(
       deserializer_class={
          "POST": MyCustomPOSTDeserializer,
          "PUT": MyCustomPUTDeserializer,
       },
    )
    def second_case_view(request, **kwargs):
       # [...]

    In the first case above, MyDeserializerClass will be applied to: POST, PUT and PATCH methods. Also, note that in second case, the deserializer_class mapping doesn't define a deserializer for the PATCH HTTP method. In this case, the "all-pass" deserializer (i.e. passes payload data to the view without any validation) will be used. The same deserializer will be applied if no deserializer_class is given.

    If the payload data doesn't respect the format defined in the deserializer, a 400 Bad Request response will be returned.

  4. allow_forms:

    A bool that allows/forbids payloads coming from forms ( application/x-www-form-urlencoded and multipart/form-data content-types).

    A 415 Unsupported Media Type response will be returned in case the user sends form data to a view decorated with allow_forms=False. The argument's default value is False.

1.2 Decorated view's arguments

As illustrated in the examples above, the @api_view decorator alters the decorated view's arguments. The decorator gathers, extracts and standardizes different arguments, then passes them to your view, in order to facilitate their use. Let's explain each argument:

@api_view
def decorated_view(
    request: HttpRequest,
    url_params: Dict[str, Any],
    query_params: Dict[str, str],
    deserialized_data: Optional[Dict[str, Any]],
) -> JsonResponse:
   # For class methods, the first argument is `self` (the class instance)
   # [...]
  1. request:

    django's native request object (django.http.request.HttpRequest). Similar to every django view's first argument. More details on django's documentation

  2. url_params:

    A dict containing the parameter defined in your view's route (django router). For example, let's take a look to url_params when requesting the URL /api/hello/foo/bar/25/ in the following example:

    # urls.py
    from django.urls import path
    from django_rest.decorators import api_view
    
    @api_view
    def hello_view(request, url_params, query_params, **kwargs):
       #  url_params = {"first_name": "foo", "last_name": "bar", "age": 25}
    
    urlpatterns = [
       path("api/hello/<str:first_name>/<str:last_name>/<int:age>/", hello_view),
    ]

    Important note: The parameters are already casted into their target types (in the example above, url_params['age'] is int, while url_params['first_name'] is str)

  3. query_params:

    A dict containing all the query parameters encoded in the request's URL. Let's request the previous example's view with the following URL:/api/hello/foo/bar/25/?lang=fr&display=true:

    # views.py
    @api_view
    def hello_view(request, url_params, query_params, **kwargs):
       #  url_params = {"first_name": "foo", "last_name": "bar", "age": 25}
       #  query_params = {"lang": "fr", "display": "true"}

    Important note: Unlike url_params, for query parameters, the values are ALWAYS strings (str), and they should be casted manually.

  4. deserialized_data:

    A dict with the data validated by the deserializer. For HTTP methods without payload (GET, DELETE, etc.), this argument's value is None.

    As explained in the section before, for HTTP methods requiring data, if no deserializer_class's been given to the decorator, deserialized_data will contain the raw payload's data (without any validation).

Note: In case you want to ignore a argument (let's say deserialized_data for a GET view), add **kwargs argument to your view. Otherwise, you'll have a arguments error.

1.3 How to decorate a view

The @api_view decorator could be applied differently on the views, depending on your use-case. You can:

  1. Decorate a function-based view. For example:

    @api_view(allowed_methods=['GET'])
    def hello_view(request, **kwargs):
       return JsonResponse({'message': 'Hello world'}, status=200)
  2. Decorate a whole class-based view (should be a sub-class of django.view.View). For example:

    @api_view(permission_class=IsStaffUser)
    class HelloView(View):
       def get(self, request, **kwargs):
          return JsonResponse({"message": "Hello world"}, status=200)
    
       def post(self, request, **kwargs):
          return JsonResponse({}, status=201)
    
       def other_method(self, arg_1, arg_2):
          # [...]

    For class-based views, the decorator decorates all view's http methods (get(), post(), put(), etc.) and ONLY them. In the example above, all http methods are restricted for staff-users only, but other_method method hasn't been altered.

Note: Both @api_view() and @api_view syntaxes are correct in case the decorator is used without arguments.

2. View Permissions

2.1 Introduction

Permissions is what determines whether a request should be granted or denied access, for a particular view. The inspection process is done before executing the decorated view's code, then, if the request satisfies the permission's constraints, the access is granted. If not, a 403 Forbidden access response is returned.

In django-REST, all permissions inherit from Permission, and passed as argument to the @api_view decorator, as seen in the previous examples.

In this section, we will start by introducing the django-REST's provided permissions, then how to build more complex permissions by combining the existing ones, and finally, how to implement your own custom permission.

2.2 Available Permissions

All the permissions listed below could be imported from django_rest.permissions

  • AllowAny: By choosing this permission, your view will be public (all requests will have granted access). It's the default permission for @api_view decorator.

  • IsAuthenticated: Allows only authenticated users to access your view. Anonymous users (i.e. not authenticated) receive a 403 Forbidden access response.

  • IsStaffUser: The view can be accessed by staff users only. A staff user is a User object having is_staff attribute set to True.

  • IsAdminUser: Admins are the only users who can access the decorated view. An admin is a User object having is_superuser attribute set to True.

  • IsReadOnly: Only HTTP safe methods (GET, HEAD and OPTIONS) are allowed. For a POST request for example, the user receives a 403 Forbidden access.

    This permission is not meant to be used in standalone, because, remember, the @api_view decorator has already the allowed_methods argument for this purpose, that returns a 405 Method not allowed. It has been implemented only to be combined with other permissions in order to build a more complex ones (the next permission on the list is a good example).

  • IsAuthenticatedOrReadOnly: This permission allows Authenticated users to use all HTTP methods (GET, POST, DELETE, etc.), and anonymous users to use safe methods only (GET, HEAD and OPTIONS).

2.3 Permissions Operators

Using logical operators allows you to combine different Permission sub-classes, in a simple and powerful way, to obtain more complex and complete permissions.

django-REST provides you 4 logical operators: AND (&), OR (|), XOR (^) and NOT (~).

Let's demonstrate those operators then their combinations in concrete examples:

  1. AND operator:

Let's create a new IsStaffAndReadOnly permission that grants access to:

  • Staff users, and only with reading http methods (GET, HEAD and OPTIONS).

It will be implemented as the following:

from django_rest.decorators import api_view
from django_rest.permissions import IsReadOnly, IsStaffUser

IsStaffAndReadOnly = IsStaffUser & IsReadOnly

@api_view(permission_class=IsStaffAndReadOnly)
def target_view(request, **kwargs):
    # [...]
  1. OR operator:

Let's create a new IsAdminOrReadOnly permission granting access to:

  • Admin users with all HTTP methods
  • Non-admin users with reading http methods (GET, HEAD and OPTIONS) only.
from django_rest.decorators import api_view
from django_rest.permissions import IsAdminUser, IsReadOnly

IsAdminOrReadOnly = IsAdminUser | IsReadOnly

@api_view(permission_class=IsAdminOrReadOnly)
def target_view(request, **kwargs):
    # [...]
  1. XOR (eXclusive OR) operator:

For this example, let's implement a permission that grants access to:

  • Users that are staff
  • Users that are not admins

Note that this permission could be implemented differently (and in a more correct and readable way). The use of XOR operator here is for demonstration purpose only. The correct implementation is shown below in "5. Combining Operators" example.

from django_rest.decorators import api_view
from django_rest.permissions import IsAdminUser, IsStaffUser

IsStaffAndNotAdminUser = IsAdminUser ^ IsStaffUser

@api_view(permission_class=IsStaffAndNotAdminUser)
def target_view(request, **kwargs):
    # [...]
  1. NOT operator:

Let's consider a view that should be exposed to anonymous (i.e. not authenticated) users only. This view's permission will be defined as the following:

from django_rest.decorators import api_view
from django_rest.permissions import IsAuthenticated

AnonymousUserOnly = ~IsAuthenticated

@api_view(permission_class=AnonymousUserOnly)
def target_view(request, **kwargs):
    # [...]
  1. Combining Operators:

Let's re-implement the IsStaffAndNotAdminUser used in the XOR example above, by using multiple operators. Then, we'll re-use it (IsStaffAndNotAdminUser) to implement a new IsStaffAndNotAdminUserOrReadOnly:

from django_rest.decorators import api_view
from django_rest.permissions import IsAdminUser, IsReadOnly, IsStaffUser

IsStaffAndNotAdminUser = IsStaffUser & (~ IsAdminUser)

IsStaffAndNotAdminUserOrReadOnly = IsStaffAndNotAdminUser | IsReadOnly
# Or: IsStaffAndNotAdminUserOrReadOnly = (IsStaffUser & (~ IsAdminUser)) | IsReadOnly

@api_view(permission_class=IsStaffAndNotAdminUserOrReadOnly)
def target_view(request, **kwargs):
    # [...]

2.4 Implement your own permission

Even if combining standard permissions covers the most usual use-cases, you may have some unusual constrains that cannot be tackled using existing operators only.

django-REST provides you a way to implement a custom permission that fits your needs. All you have to do is inherit from django_rest.permissions.BasePermission, then implement the has_permission() method.

The has_permission() takes the request object, and the target view object as arguments, and should return a bool that represents if the access should be granted (True) or not (False).

def has_permission(self, request: HttpRequest, view:Union[Callable, View]) -> bool:

Let's implement a custom permission that grants access to authenticated users having gmail address only. The "authenticated users" part will be taken care of using the existing IsAuthenticated permission.

from django_rest.decorators import api_view
from django_rest.permissions import BasePermission, IsAuthenticated


class HasGmailAddress(BasePermission):
    def has_permission(self, request, view):
        user_email = request.user.email
        domain_name = user_email.split('@')[1]
        has_gmail_address = (domain_name == 'gmail.com')
        return has_gmail_address


@api_view(permission_class=IsAuthenticated & HasGmailAddress)
def target_view(request, **kwargs):
    # [...]

Important Note:

While using operators, operands order matters.

In the example above, in HasGmailAddress code, we assumed that the user is already authenticated, instead of manually checking it. That's because if the permission IsAuthenticated isn't satisfied, django-REST returns a 403 Forbidden access before even evaluating HasGmailAddress permission. That's why in HasGmailAddress code, we assumed the user is authenticated.

If we switched permissions order as the following:

@api_view(permission_class=HasGmailAddress & IsAuthenticated)
def target_view(request, **kwargs):
    # [...]

We should have added a condition in HasGmailAddress to verify if the user is authenticated (and therefore, IsAuthenticated permission will be useless). Otherwise, if an anonymous user requests the view, a AttributeError: 'NoneType' object has no attribute 'email' exception will be raised.

3. Deserializers

3.1. Introduction

In django-REST, a deserializer validates input data (request payload and/or form data) based on custom fields ans constrains defined in the deserializer class, then "translates" data into the target format (Python primitive types), and finally executes some post-validation methods (if defined). In this chapter, we'll cover how to implement a simple deserializer, what are the fields available for use, how to nest deserializers for more complex validation and to post-clean your data, and finally, what the AllPassDeserializer is.

3.2. Implement a new Deserializer

Defining a new Deserializer is quite simple. All you need to do is to inherit from Deserializer class:

from django_rest.deserializers import Deserializer


class MyCustomDeserializer(Deserializer):
    pass

But, a deserializer class has no purpose without its fields. Let's define a simple Deserializer with 2 fields: a positive integer primary key (pk), and a username (string).

from django_rest.deserializers import fields, Deserializer


class MyCustomDeserializer(Deserializer):
    pk = fields.IntegerField(min_value=0)  # Implicit required
    username = fields.CharField(required=True)  # Explicit required

That's how a Deserializer is defined. Now, if you want to use the deserializer outside the @api_view's deserializer_class argument, you have two approaches to proceed:

The first approach

  1. Instantiate your deserializer class, passing data argument to the constructor.
  2. Check your data validity, by calling .is_valid() method.
  3. Retrieve the validated data with .data (or errors with .errors) attribute.

Here is a simple example:

from django_rest.deserializers import fields, Deserializer


class MyCustomDeserializer(Deserializer):
    pk = fields.IntegerField(min_value=0)
    username = fields.CharField(required=True)


valid_input = {'pk': '3', 'username': 'foobar'}
invalid_input = {'pk': -3, 'username': 'foobar'}

valid_instance = MyCustomDeserializer(data=valid_input)
valid_instance.is_valid()  # True
valid_instance.data  # {'pk': 3, 'username': 'foobar'}

invalid_instance = MyCustomDeserializer(data=invalid_input)
invalid_instance.is_valid()  # False
invalid_instance.errors  # {"pk": ["Ensure this value is greater than or equal to 0."]}

The second approach

  1. Instantiate the deserializer class without arguments.
  2. Call the .clean() method with the data to validate, it should return the valid data, or raise a ValidationError in case the input data is invalid.
  3. Put the clean call inside a try/except clause to catch the validation errors.

Here is a simple example:

from django_rest.deserializers import fields, Deserializer, ValidationError


class MyCustomDeserializer(Deserializer):
    pk = fields.IntegerField(min_value=0)
    username = fields.CharField(required=True)


valid_input = {'pk': '3', 'username': 'foobar'}
invalid_input = {'pk': -3, 'username': 'foobar'}

deserializer_instance = MyCustomDeserializer()

deserializer.clean(valid_input)  # {'pk': 3, 'username': 'foobar'}

try:
    data = deserializer.clean(invalid_input)
except ValidationError as errors:
    errors = dict(errors)  # errors = {"pk": ["Ensure this value is greater than or equal to 0."]}
    do_something_with_errors(errors)
else:
    do_something(data)

3.3. Available Deserializer Fields

django-REST deserializers use native django forms fields. Depending on the django version you are using, you may have access (or not) to some fields, and some of their attributes. More details on django's official doc.

Important Note: You can enjoy every feature available in django forms fields, such as custom validators and custom error messages

3.4 Nested Deserializers

django-REST offers support for nesting deserializers, in order to build more complex ones, in a flexible way and without losing readability.

By nesting deserializers, errors are nested, and output data is a nested dict too. The following example illustrates how to nest deserializers:

from django.core.validators import MinValueValidator, MaxValueValidator
from django_rest.deserializers import fields, Deserializer


class RaceDriverDeserializer(Deserializer):
    first_name = fields.CharField(required=True)
    last_name = fields.CharField(required=True)
    birth_day = fields.DateField()


class RaceCarDeserializer(Deserializer):
    brand = fields.CharField()
    model = fields.CharField()
    production_year = fields.IntegerField(
        required=False, validators=[MinValueValidator(1900), MaxValueValidator(2020)]
    )
    driver = RaceDriverDeserializer(required=True)


valid_data = {
    "brand": "Mercedes",
    "model": "C11",
    "production_year": "1990",
    "driver": {
        "first_name": "Michael",
        "last_name": "Schumacher",
        "birth_day": "1969-01-03",
    },
}

deserializer = RaceCarDeserializer(data=valid_data)
deserializer.is_valid()  # True
deserializer.data
"""
{
   'brand': 'Mercedes',
   'model': 'C11',
   'production_year': 1880,
   'driver': {
      'first_name': 'Michael',
      'last_name': 'Schumacher',
      'birth_day': datetime.date(1969, 1, 3),
   },
}
"""

invalid_data = {
    "brand": "Mercedes",
    "production_year": "1990",
    "driver": {"birth_day": "1969-01-03",},
}

deserializer = RaceCarDeserializer(data=invalid_data)
deserializer.is_valid()  # False
deserializer.errors
"""
{
   "model": ["This field is required."],
   "driver": {
      "first_name": ["This field is required."],
      "last_name": ["This field is required."],
   }
}
"""

Note that a Deserializer is a field too, it can be used the exact same way you use a field (with the same arguments).

3.5. Post-clean methods

A post-clean method is a deserializer's method, specific to a single Field and that will be called once the "standard" validation is done by the deserializer, allowing you to handle this validated value more easily, then return the value that will appear in the output data (that will be given to your view). By convention, their name follows the pattern: post_clean_<FIELD NAME>.

For example, if your deserializer defines a foo field as a CharField(), and you want that your view receives a custom transformation of that foo field (for example, let's say: striping border spaces), the post-clean method for that field should be named post_clean_foo():

from django_rest.deserializers import fields, Deserializer


class FooDeserializer(Deserializer):
   foo = fields.CharField(required=True)

   def post_clean_foo(self, cleaned_value):
      return cleaned_value.strip()

Important Note: The post-clean methods are called only if the field's standard validation succeeds. If a ValidationError occurs, the post-clean won't be done.

3.6 All-pass Deserializer

The AllPassDeserializer, is a particular deserializer that allows all payloads to pass to the view, without any validation: No type-casts, no post-clean methods, and more importantly, never raises a ValidationError or returns a 400 BadRequest.

The AllPassDeserializer is the default deserializer used by @api_view decorator. (You probably won't need it unless you're dealing with a very unusual use-case)

4. Serializers

4.1. Introduction

Serializers allow complex data such as querysets and model instances to be converted into native Python data-types, so that they could be easily rendered into JSON. Serializers do the opposite of Deserializers, and intervene at the "return" statement of your view.

4.2. Implement a new Serializer

Similar to how we've implemented a Deserializer, in order to implement your own serializer, you have to inherit from Serializer class, then define the fields that you want to include into your serialized data (probably your view's response). Here is a simple example:

from django.http import JsonResponse

from django_rest.decorators import api_view
from django_rest.http import status
from django_rest.serializers import fields, Serializer

from .models import Subscription


class SubscriptionSerializer(Serializer):
    id = fields.IntegerField(required=True)
    user_id = fields.IntegerField()
    started_at = fields.CharField(attr_name="created")
    invoices_urls = fields.ListField(fields.CharField(required=True))

@api_view
def subscription_details_view(request, url_params, **kwargs):
   subscription = Subscription.objects.get(id=url_params["subscription_pk"])
   return JsonResponse(
      SubscriptionSerializer(subscription, many=False).data,
      status=status.HTTP_200_OK,
   )

Serializer class accepts 2 arguments:

  1. instance: The object (or iterable of objects) to be serialized.
  2. many: Boolean that tells the Serializer if the object is iterable or not. If many=True, the serialized data will be a list of serialized elements of the instance iterable. Its set by default to False.

4.3. Available Serializer Fields

Serializers fields are very limited, because, remember that the data will be converted into native Python data-types (that are limited too). Besides primitive fields (CharField, IntegerField, FloatField, BooleanField), django-REST provides 3 additional fields to use within Serializers: ListField, ConstantField and MethodField (and the nested serializers). Let's dive into existing fields details.

Note In order to simplify the wording in this section, "field" word refers to the serializer's field, and "attribute" word to an attribute of the object to serialize.

1. Primitive types

The primitive types are serializer's fields that cast your data into Python's native data-types: str, int, float and bool. The CharField, IntegerField, FloatField and BooleanField accept the same arguments:

BooleanField(attr_name: str = None, label: str = None, call: bool = False, required: bool = True)
CharField(attr_name: str = None, label: str = None, call: bool = False, required: bool = True)
FloatField(attr_name: str = None, label: str = None, call: bool = False, required: bool = True)
IntegerField(attr_name: str = None, label: str = None, call: bool = False, required: bool = True)
  1. attr_name: It refers to the object's attribute that should be binded to the current field. The default value is the field name. For example:

    class Example:
        def __init__(self, foo, bar):
           self.foo = foo
           self.bar = bar
    
    class ExampleSerializer(Serializer):
        foo = fields.IntegerField()  # if `attr_name` is omitted, this field will lookup for your object's `.foo` attribute value
        whatever = fields.CharField(attr_name="bar")  # this field will store your object's `.bar` attribute's value
    
    ExampleSerializer(Example(foo=3, bar="test")).data  # {'foo': 3, 'whatever': 'test'}
  2. label: It's the name you want to give to your field in the serialized object. If omitted, it preserves the field's name. For the same Example class defined above, let's use label attribute:

    class ExampleSerializer(Serializer):
        foo = fields.IntegerField(label='integerFoo')
        whatever = fields.CharField(attr_name='bar', label='textBar')
    
    ExampleSerializer(Example(foo=3, bar="test")).data  # {'integerFoo': 3, 'textBar': 'test'}
  3. call: If set to True, the serializer will try to execute (call) your attribute. This is useful when the attribute referred-to is a method. Here is a quick example:

    class Example:
        def __init__(self, foo):
            self.foo = foo
    
        def _get_text(self):
            return "Hello"
    
        def bar(self):
            return self._get_text() + " World!"
    
    class ExampleSerializer(Serializer):
        foo = fields.IntegerField()
        whatever = fields.CharField(attr_name="bar", call=True)  # 'bar' is callable
    
    ExampleSerializer(Example(foo=3)).data  # {'foo': 3, 'whatever': 'Hello World!'}
  4. required: When set to True, if the serializer fails to retrieve the attribute's value, or to convert it into the target type, a SerializationError will be raised. If the fields isn't required (required=False), in case the serializer fails to render the attribute's value, the field won't be added to the final result. If we take the same Example class from the previous examples:

    class Example:
        def __init__(self, foo, bar):
            self.foo = foo
            self.bar = bar
    
    class ExampleSerializer(Serializer):
        foo = fields.IntegerField()
        bar = fields.IntegerField(required=True)  # Trying to fit a string into `IntegerField`
    
    ExampleSerializer(Example(foo=3, bar="test")).data  # raises a `SerializationError`
    
    class ExampleSerializer(Serializer):
        foo = fields.IntegerField()
        bar = fields.IntegerField(required=False)  # Trying to fit a string into `IntegerField`
    
    ExampleSerializer(Example(foo=3, bar="test")).data  # {'foo': 3}

2. MethodField

There are some situations in which you'd need a calculated value (from one or multiple attributes), without polluting your view, nor your model with a new method. In that case, MethodField could be very useful. By defining a MethodField, you have to define a method in your Serializer, that receives your object as input, and has to return the value to be rendered

Note MethodField is very similar to a deserializer's post-clean method, the only difference is that the post-clean receives the attribute's value, while the MethodField receives the whole object.

Here is a simple example that illustrates how MethodField works:

from django_rest.serializers import fields, Serializer

TAX_RATE = 20

class PricingExample:
    def __init__(self, initial_price):
        self.initial_price = initial_price

class PricingSerializer(Serializer):
    initial_price = fields.FloatField()
    final_price = fields.MethodField(method_name="calculate_final_price", required=True)

    def calculate_final_price(self, obj: PricingExample):
        tax_price = obj.initial_price * (TAX_RATE / 100)
        return obj.initial_price + tax_price

PricingSerializer(PricingExample(initial_price=200)).data  # {'initial_price': 200.0, 'final_price': 240.0}

MethodField accepts 3 arguments:

MethodField(label: str = None, required: bool = True, method_name: str = None)
  1. label: The same as primitive fields label.
  2. required: The same as primitive fields label.
  3. method_name: The name of the serializer's method that should be called. The default value is get_<Serializer's field name> (in the previous example, if method_name was not given, the method should have been renamed get_final_price(self, obj))

Important note: The MethodField's method should return native Python data-types (str, bool, int, float, None) or (nested) list/dict of native types.

3. ConstantField

ConstantField allows you to include constant data in your response, without having to include that constant in your model. In the previous example, TAX_RATE was a constant. In case we wanted to include it in the serialized data, we should had defined it as PricingExample class/instance attribute, or created a MethodField that returns a constant. Both solutions are quite "painful". Using ConstantField, the code will look like:

from django_rest.serializers import fields, Serializer

TAX_RATE = 20

class PricingExample:
    def __init__(self, initial_price):
        self.initial_price = initial_price

class PricingSerializer(Serializer):
    initial_price = fields.FloatField()
    final_price = fields.MethodField(method_name="calculate_final_price", required=True)
    tax_rate = fields.ConstantField(constant=TAX_RATE)

    def calculate_final_price(self, obj: PricingExample):
        tax_price = obj.initial_price * (TAX_RATE / 100)
        return obj.initial_price + tax_price

PricingSerializer(PricingExample(initial_price=200)).data  # {'initial_price': 200.0, 'final_price': 240.0, 'tax_rate': 20}

ConstantField accepts 3 arguments:

ConstantField(label: str = None, required: bool = True, constant: Any = None)
  1. label: The same as primitive fields label.
  2. required: The same as primitive fields label.
  3. constant: The constant to be included in the serialized object. The constant should be primitive (i.e. str, bool, int, float, None or combinations -list/dict- of them), otherwise SerializationError will be raised (unless required is set to False, in that case, the field won't figure in the rendered object).

4. ListField

ListField allows you to serialize iterables of primitives. Let's say your object's attribute is a list of integers. With a simple IntegerField, you won't be able to serialize that field. It could be achieved with MethodField, but it will be too much written code for a trivial thing. ListField does the same thing as the many=True for Serializer class, but the many argument isn't implemented for IntegerField, BooleanField, FloatField and CharField for performance purpose. The ListField accepts a single argument which is the field to be rendered as list.

ListField(Union[BooleanField, CharField, FloatField, IntegerField])

Here is a simple example:

from django_rest.serializers import fields, Serializer


class Path:
   def __init__(self):
      self.x_coordinates = [1.0, 1.2, 1.5, 1.8, 2.3, 8.6]
      self.y_coordinates = [19.0, 20.9, 30.1, 15.0, 22.3, 5.0]


class PathSerializer(Serializer):
   xs = fields.ListField(
      fields.FloatField(label='path_xs', attr_name='x_coordinates', required=True)
   )
   ys = fields.ListField(
      fields.FloatField(label='path_ys', attr_name='y_coordinates', required=True)
   )

PathSerializer(Path()).data  # {'path_xs': [1.0, 1.2, 1.5, 1.8, 2.3, 8.6], 'path_ys': [19.0, 20.9, 30.1, 15.0, 22.3, 5.0]}

4.4. Nested Serializers

Similarly to Deserializer, Serializer sub-classes could be nested (i.e. using Serializer sub-class as a serializer's field). Here is a simple example that shows how to nest serializers:

from datetime import datetime

from django_rest.serializers import fields, Serializer


class Invoice:
    def __init__(self, id, date):
        self.id = id
        self.created_at = date


class Subscription:
    def __init__(self):
        self.name = "foo bar subscription"
        self.invoices = [Invoice(id=i, date=datetime.now()) for i in range(10)]


class InvoiceSerializer(Serializer):
    id = fields.IntegerField()
    created = fields.CharField(attr_name="created_at")


class SubscriptionSerializer(Serializer):
    name = fields.CharField()
    invoices = InvoiceSerializer(many=True)


SubscriptionSerializer(instance=Subscription()).data  # {'name': 'foo bar subscription', 'invoices': [{'id': 0, 'created': '2020-06-08 15:26:15.414524'}, ..., {'id': 9, '2020-06-08 15:26:15.93843'}]}

4.5. DictSerializer

A DictSerializer is a sub-class of Serializer (it means that it's a particular serializer), that, instead of taking an object (class instance) as input, it takes a dict. The DictSerializer transforms a dict into another dict. It accepts the same fields as the classic serializer. Here is the previous example, rewritten using DictSerializer (to show the difference):

from datetime import datetime

from django_rest.serializers import fields, DictSerializer


subscription = {
    "name": "foo bar subscription",
    "invoices": [
        {"id": 0, "created_at": "2020-06-08 15:26:15.414524"},
        {"id": 9, "created_at": "2020-06-08 15:26:15.93843"},
    ],
}


class InvoiceSerializer(DictSerializer):
    id = fields.IntegerField()
    created = fields.CharField(attr_name="created_at")


class SubscriptionSerializer(DictSerializer):
    name = fields.CharField()
    invoices = InvoiceSerializer(many=True)


SubscriptionSerializer(instance=subscription).data  # {'name': 'foo bar subscription', 'invoices': [{'id': 0, 'created': '2020-06-08 15:26:15.414524'}, ..., {'id': 9, '2020-06-08 15:26:15.93843'}]}

5. Exceptions

5.1. @api_view exceptions catching

The @api_view decorator catches exceptions for you in case you did not, and returns a JSON response with the correct status code. If the raised exception is a sub-class of django_rest.http.exceptions.BaseAPIException, a custom message and status code will be returned. If it's not the case, the returned JSON response will have "An unknown server error occured." as message, and 500 as status code.

By raising one of the existing API exceptions (or defining your own), the decorator will return the response with the correct message (and status code). This approach ensures that:

  1. Your responses are standardized in all your decorated views: always the same message and status code for the same situations.
  2. Your view's code is lighter (dropping all the useless try/except clauses).

Here is a simple example of a view that receives url_params, calls a find_results() function, and returns a 404 in case there is no result:

from django_rest.decorators import api_view
from django_rest.http.exceptions import NotFound

@api_view
def user_custom_view(request, url_params, **kwargs):
    results = find_results(**url_params)
    if results is None or len(results) == 0:
        raise NotFound

    # In case the `find_results()` returned non-empty results:
    # [....]

5.2. Existing API Exceptions

As seen in the previous chapter, django-REST provides you some custom exceptions that you can use (i.e. raise) so that your view returns an error response, without having to do it manually everytime. Here is the list of the available API exceptions , each with its returned object and status code:

  • BadRequest:

    Response message: "Bad request." - status code: 400

  • NotAuthenticated:

    Response message: "Unauthorized operation. Maybe forgot the authentication step ?" - status code: 401

  • PermissionDenied:

    Response message: "Forbidden operation. Make sure you have the right permissions." - status code: 403

  • NotFound:

    Response message: "The requested resource is not found." - status code: 404

  • MethodNotAllowed:

    Response message: "HTTP Method not allowed." - status code: 405

  • UnsupportedMediaType:

    Response message: "Unsupported Media Type. Check your request's Content-Type." - status code: 415

  • InternalServerError:

    Response message: "An unknown server error occured." - status code: 500

  • ServiceUnavailable:

    Response message: "The requested service is unavailable." - status code: 502

5.3. Define your own API Exception

In order to define your own API Exception, all you have to do is inheriting from django_rest.http.exceptions.BaseAPIException (or one of its sub-classes), then override its STATUS_CODE and RESPONSE_MESSAGE attributes.

Here is a simple example that shows how to define a conflict exception:

from django_rest.decorators import api_view
from django_rest.http import exceptions, status

class Conflict(exceptions.BaseAPIException):
    STATUS_CODE = status.HTTP_409_CONFLICT
    RESPONSE_MESSAGE = "Requests conflict."

@api_view
def my_conflicting_view(request, **kwargs):
    # [...]
    if some_condition_is_satisfied:
        raise Conflict  # returns JsonResponse({'error_msg': 'Requests conflict.'}, status=409)
    # [....]

6. HTTP

django-REST provides some constants/enumerations that allow you to avoid using hard-coded values (str for HTTP methods, and int for status codes), and improve your code readability.

6.1. HTTP Status codes

The HTTP status codes can be imported from django_rest.http.status:

from django_rest.http.status import HTTP_200_OK

response_status = HTTP_200_OK

# OR
from django_rest.http import status

response_status = status.HTTP_200_OK

Here is the exhaustive list of http status constants provided by django-REST (more details about status codes here):

  • HTTP_100_CONTINUE
  • HTTP_101_SWITCHING_PROTOCOLS
  • HTTP_200_OK
  • HTTP_201_CREATED
  • HTTP_202_ACCEPTED
  • HTTP_203_NON_AUTHORITATIVE_INFORMATION
  • HTTP_204_NO_CONTENT
  • HTTP_205_RESET_CONTENT
  • HTTP_206_PARTIAL_CONTENT
  • HTTP_207_MULTI_STATUS
  • HTTP_208_ALREADY_REPORTED
  • HTTP_226_IM_USED
  • HTTP_300_MULTIPLE_CHOICES
  • HTTP_301_MOVED_PERMANENTLY
  • HTTP_302_FOUND
  • HTTP_303_SEE_OTHER
  • HTTP_304_NOT_MODIFIED
  • HTTP_305_USE_PROXY
  • HTTP_306_RESERVED
  • HTTP_307_TEMPORARY_REDIRECT
  • HTTP_308_PERMANENT_REDIRECT
  • HTTP_400_BAD_REQUEST
  • HTTP_401_UNAUTHORIZED
  • HTTP_402_PAYMENT_REQUIRED
  • HTTP_403_FORBIDDEN
  • HTTP_404_NOT_FOUND
  • HTTP_405_METHOD_NOT_ALLOWED
  • HTTP_406_NOT_ACCEPTABLE
  • HTTP_407_PROXY_AUTHENTICATION_REQUIRED
  • HTTP_408_REQUEST_TIMEOUT
  • HTTP_409_CONFLICT
  • HTTP_410_GONE
  • HTTP_411_LENGTH_REQUIRED
  • HTTP_412_PRECONDITION_FAILED
  • HTTP_413_REQUEST_ENTITY_TOO_LARGE
  • HTTP_414_REQUEST_URI_TOO_LONG
  • HTTP_415_UNSUPPORTED_MEDIA_TYPE
  • HTTP_416_REQUESTED_RANGE_NOT_SATISFIABLE
  • HTTP_417_EXPECTATION_FAILED
  • HTTP_418_IM_A_TEAPOT
  • HTTP_422_UNPROCESSABLE_ENTITY
  • HTTP_423_LOCKED
  • HTTP_424_FAILED_DEPENDENCY
  • HTTP_426_UPGRADE_REQUIRED
  • HTTP_428_PRECONDITION_REQUIRED
  • HTTP_429_TOO_MANY_REQUESTS
  • HTTP_431_REQUEST_HEADER_FIELDS_TOO_LARGE
  • HTTP_451_UNAVAILABLE_FOR_LEGAL_REASONS
  • HTTP_500_INTERNAL_SERVER_ERROR
  • HTTP_501_NOT_IMPLEMENTED
  • HTTP_502_BAD_GATEWAY
  • HTTP_503_SERVICE_UNAVAILABLE
  • HTTP_504_GATEWAY_TIMEOUT
  • HTTP_505_HTTP_VERSION_NOT_SUPPORTED
  • HTTP_506_VARIANT_ALSO_NEGOTIATES
  • HTTP_507_INSUFFICIENT_STORAGE
  • HTTP_508_LOOP_DETECTED
  • HTTP_509_BANDWIDTH_LIMIT_EXCEEDED
  • HTTP_510_NOT_EXTENDED
  • HTTP_511_NETWORK_AUTHENTICATION_REQUIRED

Besides, you also have (in the same module django_rest.http.status) 5 functions that you can use to verify a status code category easily:

  • is_informational(code: int) -> bool
  • is_success(code: int) -> bool
  • is_redirect(code: int) -> bool
  • is_client_error(code: int) -> bool
  • is_server_error(code: int) -> bool

6.2. HTTP Methods

All the following HTTP method's related constants can be found in django_rest.http.methods:

String constants:

  • HEAD
  • GET
  • POST
  • PUT
  • PATCH
  • DELETE
  • OPTIONS
  • TRACE
  • CONNECT

Tuple constants:

  • SAFE_METHODS = (GET, HEAD, OPTIONS)
  • SUPPORTING_PAYLOAD_METHODS = (POST, PUT, PATCH)
  • ALL_METHODS = (HEAD, GET, POST, PUT, PATCH, DELETE, OPTIONS, TRACE, CONNECT)

— Made with ♥️