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
django-REST library requires:
- Python version 2.7+ or 3.3+
- django version 1.10+
You can get the package using pip
, as the following:
pip install django-rest
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": "myfirst@user.com",
"id": 1,
"is_staff": true,
"username": "firstuser"
}
]
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,
)
-
permission_class
A class that defines who is allowed to access the decorated view. If no
permission_class
given, the decorator's default permission isAllowAny
(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. -
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 indjango_rest.http.methods
. If noallowed_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. -
deserializer_class:
Could be either a sub-class of
Deserializer
(as shown in the previous example), or adict
that maps HTTP methods that use payload (i.e.POST
,PUT
andPATCH
) toDeserializer
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
andPATCH
methods. Also, note that in second case, thedeserializer_class
mapping doesn't define a deserializer for thePATCH
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 nodeserializer_class
is given.If the payload data doesn't respect the format defined in the deserializer, a
400 Bad Request
response will be returned. -
allow_forms:
A
bool
that allows/forbids payloads coming from forms (application/x-www-form-urlencoded
andmultipart/form-data
content-types).A
415 Unsupported Media Type
response will be returned in case the user sends form data to a view decorated withallow_forms=False
. The argument's default value isFalse
.
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)
# [...]
-
request:
django's native request object (
django.http.request.HttpRequest
). Similar to every django view's first argument. More details on django's documentation -
url_params:
A
dict
containing the parameter defined in your view's route (django router). For example, let's take a look tourl_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']
isint
, whileurl_params['first_name']
isstr
) -
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. -
deserialized_data:
A
dict
with the data validated by the deserializer. For HTTP methods without payload (GET
,DELETE
, etc.), this argument's value isNone
.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.
The @api_view
decorator could be applied differently on the views, depending on your use-case. You can:
-
Decorate a function-based view. For example:
@api_view(allowed_methods=['GET']) def hello_view(request, **kwargs): return JsonResponse({'message': 'Hello world'}, status=200)
-
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, butother_method
method hasn't been altered.
Note: Both @api_view()
and @api_view
syntaxes are correct in case the decorator is used without arguments.
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.
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 havingis_staff
attribute set toTrue
. -
IsAdminUser: Admins are the only users who can access the decorated view. An admin is a
User
object havingis_superuser
attribute set toTrue
. -
IsReadOnly: Only HTTP safe methods (
GET
,HEAD
andOPTIONS
) are allowed. For aPOST
request for example, the user receives a403 Forbidden access
.This permission is not meant to be used in standalone, because, remember, the
@api_view
decorator has already theallowed_methods
argument for this purpose, that returns a405 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
andOPTIONS
).
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:
- AND operator:
Let's create a new IsStaffAndReadOnly
permission that grants access to:
- Staff users, and only with reading http methods (
GET
,HEAD
andOPTIONS
).
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):
# [...]
- 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
andOPTIONS
) 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):
# [...]
- 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):
# [...]
- 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):
# [...]
- 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):
# [...]
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.
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.
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:
- Instantiate your deserializer class, passing
data
argument to the constructor. - Check your data validity, by calling
.is_valid()
method. - 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."]}
- Instantiate the deserializer class without arguments.
- Call the
.clean()
method with the data to validate, it should return the valid data, or raise aValidationError
in case the input data is invalid. - 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)
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
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).
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.
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)
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.
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:
- instance: The object (or iterable of objects) to be serialized.
-
many: Boolean that tells the
Serializer
if the object is iterable or not. Ifmany=True
, the serialized data will be alist
of serialized elements of theinstance
iterable. Its set by default toFalse
.
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.
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)
-
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'}
-
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 uselabel
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'}
-
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!'}
-
required: When set to
True
, if the serializer fails to retrieve the attribute's value, or to convert it into the target type, aSerializationError
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 sameExample
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}
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)
-
label: The same as primitive fields
label
. -
required: The same as primitive fields
label
. -
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, ifmethod_name
was not given, the method should have been renamedget_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.
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)
-
label: The same as primitive fields
label
. -
required: The same as primitive fields
label
. -
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), otherwiseSerializationError
will be raised (unlessrequired
is set toFalse
, in that case, the field won't figure in the rendered object).
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]}
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'}]}
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'}]}
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:
- Your responses are standardized in all your decorated views: always the same message and status code for the same situations.
- 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:
# [....]
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
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)
# [....]
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.
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
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