Serializers take the data that exist on the server and serialize it and output into a format that can be read by other technologies. If data is sent to the server, it packages the data up so that the data can be read by the data models on the server and be saved in the database.
serializer = CommentSerializer(comment)
serializer.data
// {'email': 'leila@example.com', 'content': 'foo bar', 'created': '2016-01-27T15:17:10.375877'}
json = JSONRenderer().render(serializer.data)
json
// b'{"email":"leila@example.com","content":"foo bar","created":"2016-01-27T15:17:10.375877"}'
import io
from rest_framework.parsers import JSONParser
stream = io.BytesIO(json)
data = JSONParser().parse(stream)
serializer = CommentSerializer(data=data)
serializer.is_valid()
// True
serializer.validated_data
// {'content': 'foo bar', 'email': 'leila@example.com', 'created': datetime.datetime(2012, 08, 22, 16, 20, 09, 822243)}
Long way, more customizable
Shorter way, less customizable
When deserializing data, you always need to call is_valid()
before attempting to access the validated data or save an object instance. If any validation errors do occur, the .errors
property will contain a dictionary representing the resulting error messages.
Of course, DRF provides built-in validators such as UniqueValidator
, UniqueTogetherValidator
, UniqueForDateValidator
, etc. They can be used by specifying the aforementioned validators in class Meta
of serializer classes:
class Meta:
validators = [UniqueTogetherValidator(queryset=ToDoItem.objects.all(), fields=('list', 'position'))]
However, there will be times when they are not enough, and you have to customize your own validators.
is_valid()
method takes an optional raise_exception
flag which will cause it to raise a serializers.ValidationError
exception if there are validation errors.
Validation erors are automatically dealt with by the default exception handler that DRF provides, which will return HTTP 400 Bad Request
responses by default.
serializer.is_valid(raise_exception=True)
Validation check on a single field. For example, check if specific words are not used in an Article instance's description field. Add .validate_<field_name>
methods to your Serializer
subclass to specify custom field-level validation. These methods take a signle argument, which is the field value that requires validation. They should return the validated value or raise a ValidationError.
def validate_title(self, value):
// Check that the blog post is about Django.
if 'django' not in value.lower():
raise serializers.ValidationError("Blog post is not about Django")
return value
Validation check on object level, using multiple fields. For example, checking if an Article instance's title and descriptions of an instance are the same. Add .validate()
method to your Serializer
subclass to perform a validation that requires access to multiple fields. The argument must be a dictionary of field values.
def validate(self, data):
// Check that start is before finish.
if data['start'] > data['finish']:
raise serializers.ValidationError("finish must occur after start")
return data
ModelSerializer allows us to speed up the creation of Serializers based on models we defined, providing us all the code that is needed for the most common development use cases.
ModelSerializer
class is different from a regular Serializer
class in that:
.create()
and .update()
.By default, ForeignKey relationship is expressed using the primary key of the related object.
To display this relationship in a more explicit way, use serializer relational fields.
class Article(models.Model):
author = models.ForeignKey(Journalist, on_delete=models.CASCADE, related_name='articles')
class ArticleSerializer(serializers.ModelSerializer):
// Now the author will be displayed using its __str__() function
author = serializers.StringRelatedField()
Another way to express nested relationship within serializers is using the hyperlink related fields. This is a field that allows us to get a direct link to a proper endpoint for a specific resource.
class JournalistSerializer(serializers.ModelSerializer):
articles = serializers.HyperlinkedRelatedField(many=True, read_only=True, view_name='article-detail')
Relational fields are used to represent model relatinoships. The relational fields are declared in relations.py
, but by convention you should import them from the serializers
module, using from rest_framework import serializers
and refer to fields as serializers.<FieldName>
For more information, check out the official documentation on serializer relations.
Take, for example, an Article class which is in Foreign Key relationship with a Journalist class (Many To One). In order to handle nested relationship between these two classes, from whose serializer should the other model's serializer be called?
In this case, the call should be made from JournalistSerializer. A rule of thumb is that the caller is likely to be the model serializer of the "One" in a Many-To-One relationship. This is because the "Many" model tends to be dependent on the "One" model, and to handle a POST request within a same class based view, the dependent side should be called from the independent side, not the other way around.
EX)
class JournalistSerializer(serializers.ModelSerializer):
articles = ArticleSerializer(many=True, read_only=True)
class Meta:
model = Journalist
fields = "__all__"
// Not
class ArticleSerializer(serializers.ModelSerializer):
author = JournalistSerializer()
DRF's Request object extends the regular HttpRequest, providing more flexible request parsing.
request.data
attribute can handle arbitrary data, and works for 'POST', 'PUT', and 'PATCH' methods.
It is important to note that DRF request object is different from pure django's request object
DRF's Response object is a time of TemplateResponse
that takes unrendered content and uses content negotiation to determine the correct content type to return to the client.
From the source code:
class Response(SimpleTemplateResponse):
// An HttpResponse that allows its data to be rendered into arbitrary media types.
DRF provides more explicit identifiers for each status code other than just numeric HTTP status codes.
Ex) HTTP_400_BAD_REQUEST
in the status
module. Use these rather than using numeric identifiers.
@api_view
decorator for working with function based views.APIView
class for working with class-based views.These wrappers ensure that you receive Request
instances in your view and help add context to Response
objects so that content negotiation can be performed.
The wrappers also support returning 405 Method Not Allowed
responses when appropriate, and handling any ParseError
exception if request.data
is malformed.
Following is a rudimentary DRF view introduced in django-rest-framework.org.
@api_view(['GET', 'PUT', 'DELETE'])
def snippet_detail(request, pk):
"""
Retrieve, update or delete a code snippet.
"""
try:
snippet = Snippet.objects.get(pk=pk)
except Snippet.DoesNotExist:
return Response(status=status.HTTP_404_NOT_FOUND)
if request.method == 'GET':
serializer = SnippetSerializer(snippet)
return Response(serializer.data)
elif request.method == 'PUT':
serializer = SnippetSerializer(snippet, data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
elif request.method == 'DELETE':
snippet.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
Because with DRF, our responses are no longer hardwired to a single content type, we can add format suffixes to our API endpoints, which will be able to perceive URLs that explicitly refer to a given format.
// views.py
def snippet_detail(request, pk, format=None):
pass
// urls.py
from django.urls import path
from rest_framework.urlpatterns import format_suffix_patterns
from snippets import views
urlpatterns = [
path('snippets/', views.snippet_list),
path('snippets/<int:pk>', views.snippet_detail),
]
urlpatterns = format_suffix_patterns(urlpatterns)
Control the format of the response either by using (1) the Accept
header:
http http://127.0.0.1:8000/snippets/ Accept:application/json # Request JSON
http http://127.0.0.1:8000/snippets/ Accept:text/html # Request HTML
Or (2) by appending a format suffix:
http http://127.0.0.1:8000/snippets.json # JSON suffix
http http://127.0.0.1:8000/snippets.api # Browsable API suffix
You can also control the format of the request that we send, using the Content-Type
header.
# POST using JSON
http --json POST http://127.0.0.1:8000/snippets/ code="print(456)"
class ArticleDetailAPIView(APIView):
def get_object(self, pk):
article = get_object_or_404(Article, pk=pk)
return article
def get(self, request, pk):
article = self.get_object(pk)
serializer = ArticleSerializer(article)
return Response(serializer.data)
def put(self, request, pk):
article = self.get_object(pk)
serializer = ArticleSerializer(article, data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def delete(self, request, pk):
article = self.get_object(pk)
article.delete()
return Response(status=status.HTTP_204_NO_CONTENT)