Using the default parsing and rendering options and move beyond JSON
The APIView
class specifies default settings for each view that we can override by specifying appropriate values in the gamesapi/settings.py
file or by overriding the class attributes in subclasses. As previously explained, the usage of the APIView
class under the hoods makes the decorator apply these default settings. Thus, whenever we use the decorator, the default parser classes and the default renderer classes will be associated with the function views.
By default, the value for the DEFAULT_PARSER_CLASSES
is the following tuple of classes:
( 'rest_framework.parsers.JSONParser', 'rest_framework.parsers.FormParser', 'rest_framework.parsers.MultiPartParser' )
When we use the decorator, the API will be able to handle any of the following content types through the appropriate parsers when accessing the request.data
attribute:
application/json
application/x-www-form-urlencoded
multipart/form-data
Tip
When we access the request.data
attribute in the functions, Django REST Framework examines the value for the Content-Type
header in the incoming request and determines the appropriate parser to parse the request content. If we use the previously explained default values, the Django REST Framework will be able to parse the previously listed content types. However, it is extremely important that the request specifies the appropriate value in the Content-Type
header.
We have to remove the usage of the rest_framework.parsers.JSONParser
class in the functions to make it possible to be able to work with all the configured parsers and stop working with a parser that only works with JSON. The game_list
function executes the following two lines when request.method
is equal to 'POST'
:
game_data = JSONParser().parse(request) game_serializer = GameSerializer(data=game_data)
We will remove the first line that uses the JSONParser
and we will pass request.data
as the data argument for the GameSerializer
. The following line will replace the previous lines:
game_serializer = GameSerializer(data=request.data)
The game_detail
function executes the following two lines when request.method
is equal to 'PUT'
:
game_data = JSONParser().parse(request) game_serializer = GameSerializer(game, data=game_data)
We will make the same edits done for the code in the game_list
function. We will remove the first line that uses the JSONParser
and we will pass request.data
as the data argument for the GameSerializer
. The following line will replace the previous lines:
game_serializer = GameSerializer(game, data=request.data)
By default, the value for the DEFAULT_RENDERER_CLASSES
is the following tuple of classes:
( 'rest_framework.renderers.JSONRenderer', 'rest_framework.renderers.BrowsableAPIRenderer', )
When we use the decorator, the API will be able to render the following content types in the response, through the appropriate renderers, when working with the rest_framework.response.Response
object:
application/json
text/html
By default, the value for the DEFAULT_CONTENT_NEGOTIATION_CLASS
is the rest_framework.negotiation.DefaultContentNegotiation
class. When we use the decorator, the API will use this content negotiation class to select the appropriate renderer for the response based on the incoming request. This way, when a request specifies that it will accept text/html
, the content negotiation class selects the rest_framework.renderers.BrowsableAPIRenderer
to render the response and generate text/html
instead of application/json
.
We have to replace the usage of both the JSONResponse
and HttpResponse
classes in the functions with the rest_framework.response.Response
class. The Response
class uses the previously explained content negotiation features, renders the received data into the appropriate content type, and returns it to the client.
Now, go to the gamesapi/games
folder and open the views.py
file. Replace the code in this file with the following code that removes the JSONResponse
class and uses the @api_view
decorator for the functions and the rest_framework.response.Response
class. The modified lines are highlighted. The code file for the sample is included in the restful_python_chapter_02_02
folder:
from rest_framework.parsers import JSONParser from rest_framework import status from rest_framework.decorators import api_view from rest_framework.response import Response from games.models import Game from games.serializers import GameSerializer @api_view(['GET', 'POST']) def game_list(request): if request.method == 'GET': games = Game.objects.all() games_serializer = GameSerializer(games, many=True) return Response(games_serializer.data) elif request.method == 'POST': game_serializer = GameSerializer(data=request.data) if game_serializer.is_valid(): game_serializer.save() return Response(game_serializer.data, status=status.HTTP_201_CREATED) return Response(game_serializer.errors, status=status.HTTP_400_BAD_REQUEST) @api_view(['GET', 'PUT', 'POST']) def game_detail(request, pk): try: game = Game.objects.get(pk=pk) except Game.DoesNotExist: return Response(status=status.HTTP_404_NOT_FOUND) if request.method == 'GET': game_serializer = GameSerializer(game) return Response(game_serializer.data) elif request.method == 'PUT': game_serializer = GameSerializer(game, data=request.data) if game_serializer.is_valid(): game_serializer.save() return Response(game_serializer.data) return Response(game_serializer.errors, status=status.HTTP_400_BAD_REQUEST) elif request.method == 'DELETE': game.delete() return Response(status=status.HTTP_204_NO_CONTENT)
After you save the preceding changes, run the following command:
http OPTIONS :8000/games/
The following is the equivalent curl
command:
curl -iX OPTIONS :8000/games/
The previous command will compose and send the following HTTP request: OPTIONS http://localhost:8000/games/
. The request will match and run the views.game_list
function, that is, the game_list
function declared within the games/views.py
file. We added the @api_view
decorator to this function, and therefore, it is now capable of determining the supported HTTP verbs, parsing, and rendering capabilities. The following lines show the output:
HTTP/1.0 200 OK Allow: GET, POST, OPTIONS Content-Type: application/json Date: Thu, 09 Jun 2016 20:24:31 GMT Server: WSGIServer/0.2 CPython/3.5.1 Vary: Accept, Cookie X-Frame-Options: SAMEORIGIN { "description": "", "name": "Game List", "parses": [ "application/json", "application/x-www-form-urlencoded", "multipart/form-data" ], "renders": [ "application/json", "text/html" ] }
The response header includes an Allow
key with a comma-separated list of HTTP verbs supported by the resource collection as its value: GET, POST, OPTIONS
. As our request didn't specify the allowed content type, the function rendered the response with the default application/json
content type. The response body specifies the Content-type
that the resource collection parses and the Content-type
that it renders.
Run the following command to compose and send an HTTP request with the OPTIONS
verb for a game resource. Don't forget to replace 3
with a primary key value of an existing game in your configuration.
http OPTIONS :8000/games/3/
The following is the equivalent curl command:
curl -iX OPTIONS :8000/games/3/
The preceding command will compose and send the following HTTP request: OPTIONS http://localhost:8000/games/3/
. The request will match and run the views.game_detail
function, that is, the game_detail
function declared within the games/views.py
file. We also added the @api_view
decorator to this function, and therefore, it is capable of determining the supported HTTP verbs, parsing, and rendering capabilities. The following lines show the output:
HTTP/1.0 200 OK Allow: GET, POST, OPTIONS, PUT Content-Type: application/json Date: Thu, 09 Jun 2016 21:35:58 GMT Server: WSGIServer/0.2 CPython/3.5.1 Vary: Accept, Cookie X-Frame-Options: SAMEORIGIN { "description": "", "name": "Game Detail", "parses": [ "application/json", "application/x-www-form-urlencoded", "multipart/form-data" ], "renders": [ "application/json", "text/html" ] }
The response header includes an Allow
key with a comma-separated list of HTTP verbs supported by the resource as its value: GET, POST, OPTIONS, PUT
. The response body specifies the content-type that the resource parses and the content-type that it renders, with the same contents received in the previous OPTIONS
request applied to a resource collection, that is, to a games collection.
In Chapter 1, Developing RESTful APIs with Django, when we composed and sent POST and PUT commands, we had to use the use the -H "Content-Type: application/json"
option to tell curl to send the data specified after the -d
option as application/json
instead of the default application/x-www-form-urlencoded
. Now, in addition to application/json
, our API is capable of parsing application/x-www-form-urlencoded
and multipart/form-data
data specified in the POST
and PUT
requests. Thus, we can compose and send a POST command that sends the data as application/x-www-form-urlencoded
, with the changes made to our API.
We will compose and send an HTTP request to create a new game. In this case, we will use the -f option for HTTPie, that serializes data items from the command line as form fields and sets the Content-Type
header key to the application/x-www-form-urlencoded
value:
http -f POST :8000/games/ name='Toy Story 4' game_category='3D RPG' played=false release_date='2016-05-18T03:02:00.776594Z'
The following is the equivalent curl command. Note that we don't use the -H
option and curl will send the data in the default application/x-www-form-urlencoded
:
curl -iX POST -d '{"name":"Toy Story 4", "game_category":"3D RPG", "played": "false", "release_date": "2016-05-18T03:02:00.776594Z"}' :8000/games/
The previous commands will compose and send the following HTTP request: POST http://localhost:8000/games/
with the Content-Type
header key set to the application/x-www-form-urlencoded
value and the following data:
name=Toy+Story+4&game_category=3D+RPG&played=false&release_date=2016-05-18T03%3A02%3A00.776594Z
The request specifies /games/
, and therefore, it will match '^games/$'
and run the views.game_list
function, that is, the updated game_detail
function declared within the games/views.py
file. As the HTTP verb for the request is POST
, the request.method
property is equal to 'POST'
, and therefore, the function will execute the code that creates a GameSerializer
instance and passes request.data
as the data argument for its creation. The rest_framework.parsers.FormParser
class will parse the data received in the request, the code creates a new Game
and, if the data is valid, it saves the new Game
. If the new Game
was successfully persisted in the database, the function returns an HTTP 201 Created
status code and the recently persisted Game
serialized to JSON in the response body. The following lines show an example response for the HTTP request, with the new Game
object in the JSON response:
HTTP/1.0 201 Created Allow: OPTIONS, POST, GET Content-Type: application/json Date: Fri, 10 Jun 2016 20:38:40 GMT Server: WSGIServer/0.2 CPython/3.5.1 Vary: Accept, Cookie X-Frame-Options: SAMEORIGIN { "game_category": "3D RPG", "id": 20, "name": "Toy Story 4", "played": false, "release_date": "2016-05-18T03:02:00.776594Z" }
We can run the following command after we make the changes in the code, to see what happens when we compose and send an HTTP request with an HTTP verb that is not supported:
http PUT :8000/games/
The following is the equivalent curl
command:
curl -iX PUT :8000/games/
The previous command will compose and send the following HTTP request: PUT http://localhost:8000/games/
. The request will match and try to run the views.game_list
function, that is, the game_list
function declared within the games/views.py
file. The @api_view
decorator we added to this function doesn't include 'PUT'
in the string list with the allowed HTTP verbs, and therefore, the default behavior returns a 405 Method Not Allowed
status code. The following lines show the output along with the response from the previous request. A JSON content provides a detail
key with a string value, which indicates that the PUT
method is not allowed:
HTTP/1.0 405 Method Not Allowed Allow: GET, OPTIONS, POST Content-Type: application/json Date: Sat, 11 Jun 2016 00:49:30 GMT Server: WSGIServer/0.2 CPython/3.5.1 Vary: Accept, Cookie X-Frame-Options: SAMEORIGIN { "detail": "Method "PUT" not allowed." }