0
# Content Negotiation
1
2
Parser and renderer classes for handling multiple content types with automatic format detection and conversion in Django REST Framework.
3
4
## Capabilities
5
6
### Parser Classes
7
8
Parser classes handle incoming request data in different formats.
9
10
```python { .api }
11
class BaseParser:
12
"""
13
Base class for all parser implementations.
14
"""
15
media_type = None # Media type this parser handles
16
17
def parse(self, stream, media_type=None, parser_context=None):
18
"""
19
Parse incoming data stream.
20
21
Args:
22
stream: Input data stream
23
media_type (str): Request content type
24
parser_context (dict): Additional context
25
26
Returns:
27
Parsed data
28
29
Raises:
30
ParseError: If parsing fails
31
"""
32
raise NotImplementedError
33
34
class JSONParser(BaseParser):
35
"""
36
Parser for JSON data.
37
"""
38
media_type = 'application/json'
39
40
def parse(self, stream, media_type=None, parser_context=None):
41
"""
42
Parse JSON data from stream.
43
44
Returns:
45
dict or list: Parsed JSON data
46
47
Raises:
48
ParseError: If JSON is malformed
49
"""
50
51
class FormParser(BaseParser):
52
"""
53
Parser for HTML form data (application/x-www-form-urlencoded).
54
"""
55
media_type = 'application/x-www-form-urlencoded'
56
57
def parse(self, stream, media_type=None, parser_context=None):
58
"""
59
Parse form-encoded data.
60
61
Returns:
62
QueryDict: Parsed form data
63
"""
64
65
class MultiPartParser(BaseParser):
66
"""
67
Parser for multipart form data (multipart/form-data).
68
"""
69
media_type = 'multipart/form-data'
70
71
def parse(self, stream, media_type=None, parser_context=None):
72
"""
73
Parse multipart form data including file uploads.
74
75
Returns:
76
DataAndFiles: Container with .data and .files attributes
77
"""
78
79
class FileUploadParser(BaseParser):
80
"""
81
Parser for raw file uploads.
82
"""
83
media_type = '*/*' # Accepts any media type
84
errors = {
85
'unhandled': 'FileUploadParser can only handle a single file upload.',
86
'no_filename': 'Missing filename. Request should include Content-Disposition header with filename parameter.',
87
}
88
89
def parse(self, stream, media_type=None, parser_context=None):
90
"""
91
Parse raw file upload.
92
93
Returns:
94
dict: Dictionary with single file entry
95
96
Raises:
97
ParseError: If multiple files or no filename
98
"""
99
```
100
101
### Renderer Classes
102
103
Renderer classes serialize data into different output formats.
104
105
```python { .api }
106
class BaseRenderer:
107
"""
108
Base class for all renderer implementations.
109
"""
110
media_type = None # Media type this renderer produces
111
format = None # Format name for URL suffixes
112
charset = 'utf-8' # Character encoding
113
render_style = 'text' # Rendering style hint
114
115
def render(self, data, accepted_media_type=None, renderer_context=None):
116
"""
117
Render data into output format.
118
119
Args:
120
data: Data to render
121
accepted_media_type (str): Accepted media type from negotiation
122
renderer_context (dict): Additional context
123
124
Returns:
125
bytes: Rendered output
126
"""
127
raise NotImplementedError
128
129
class JSONRenderer(BaseRenderer):
130
"""
131
Renderer for JSON output.
132
"""
133
media_type = 'application/json'
134
format = 'json'
135
encoder_class = encoders.JSONEncoder
136
ensure_ascii = True
137
compact = True
138
strict = True
139
140
def render(self, data, accepted_media_type=None, renderer_context=None):
141
"""
142
Render data as JSON.
143
144
Returns:
145
bytes: JSON-encoded data
146
"""
147
148
class TemplateHTMLRenderer(BaseRenderer):
149
"""
150
Renderer for HTML templates.
151
"""
152
media_type = 'text/html'
153
format = 'html'
154
template_name = None
155
exception_template_names = []
156
charset = 'utf-8'
157
158
def render(self, data, accepted_media_type=None, renderer_context=None):
159
"""
160
Render data using Django template.
161
162
Returns:
163
bytes: Rendered HTML
164
"""
165
166
class StaticHTMLRenderer(TemplateHTMLRenderer):
167
"""
168
Renderer for static HTML content.
169
"""
170
def render(self, data, accepted_media_type=None, renderer_context=None):
171
"""
172
Render static HTML string.
173
174
Returns:
175
bytes: HTML content as bytes
176
"""
177
178
class BrowsableAPIRenderer(BaseRenderer):
179
"""
180
Renderer for browsable API interface.
181
"""
182
media_type = 'text/html'
183
format = 'api'
184
template = 'rest_framework/api.html'
185
filter_template = 'rest_framework/filters/base.html'
186
code_style = 'emacs'
187
charset = 'utf-8'
188
189
def render(self, data, accepted_media_type=None, renderer_context=None):
190
"""
191
Render browsable API interface.
192
193
Returns:
194
bytes: HTML for browsable API
195
"""
196
197
class HTMLFormRenderer(BaseRenderer):
198
"""
199
Renderer for HTML forms.
200
"""
201
media_type = 'application/x-www-form-urlencoded'
202
format = 'form'
203
template_pack = 'rest_framework/vertical/'
204
205
def render(self, data, accepted_media_type=None, renderer_context=None):
206
"""
207
Render HTML form for data input.
208
209
Returns:
210
str: HTML form markup
211
"""
212
213
class MultiPartRenderer(BaseRenderer):
214
"""
215
Renderer for multipart form data.
216
"""
217
media_type = 'multipart/form-data'
218
format = 'multipart'
219
BOUNDARY = 'BoUnDaRyStRiNg'
220
charset = 'utf-8'
221
222
def render(self, data, accepted_media_type=None, renderer_context=None):
223
"""
224
Render multipart form data.
225
226
Returns:
227
bytes: Multipart-encoded data
228
"""
229
230
class OpenAPIRenderer(BaseRenderer):
231
"""
232
Renderer for OpenAPI schema format.
233
"""
234
media_type = 'application/vnd.oai.openapi'
235
format = 'openapi'
236
charset = 'utf-8'
237
238
def render(self, data, accepted_media_type=None, renderer_context=None):
239
"""
240
Render OpenAPI schema.
241
242
Returns:
243
bytes: YAML-formatted OpenAPI schema
244
"""
245
246
class JSONOpenAPIRenderer(BaseRenderer):
247
"""
248
Renderer for OpenAPI schema in JSON format.
249
"""
250
media_type = 'application/vnd.oai.openapi+json'
251
format = 'openapi-json'
252
253
def render(self, data, accepted_media_type=None, renderer_context=None):
254
"""
255
Render OpenAPI schema as JSON.
256
257
Returns:
258
bytes: JSON-formatted OpenAPI schema
259
"""
260
```
261
262
### Content Negotiation
263
264
Classes that determine appropriate parser/renderer based on request.
265
266
```python { .api }
267
class BaseContentNegotiation:
268
"""
269
Base class for content negotiation.
270
"""
271
def select_parser(self, request, parsers):
272
"""
273
Select appropriate parser for request.
274
275
Args:
276
request: HTTP request
277
parsers (list): Available parser instances
278
279
Returns:
280
Parser: Selected parser instance
281
282
Raises:
283
UnsupportedMediaType: If no suitable parser found
284
"""
285
raise NotImplementedError
286
287
def select_renderer(self, request, renderers, format_suffix=None):
288
"""
289
Select appropriate renderer for response.
290
291
Args:
292
request: HTTP request
293
renderers (list): Available renderer instances
294
format_suffix (str): URL format suffix
295
296
Returns:
297
tuple: (renderer, media_type)
298
299
Raises:
300
NotAcceptable: If no acceptable renderer found
301
"""
302
raise NotImplementedError
303
304
class DefaultContentNegotiation(BaseContentNegotiation):
305
"""
306
Default content negotiation implementation.
307
"""
308
settings = api_settings
309
310
def select_parser(self, request, parsers):
311
"""
312
Select parser based on Content-Type header.
313
"""
314
315
def select_renderer(self, request, renderers, format_suffix=None):
316
"""
317
Select renderer based on Accept header and format suffix.
318
"""
319
320
def get_accept_list(self, request):
321
"""
322
Parse Accept header into list of media types with priorities.
323
324
Args:
325
request: HTTP request
326
327
Returns:
328
list: Sorted list of (media_type, priority) tuples
329
"""
330
331
def filter_renderers(self, renderers, format):
332
"""
333
Filter renderers by format suffix.
334
335
Args:
336
renderers (list): Available renderers
337
format (str): Format suffix
338
339
Returns:
340
list: Filtered renderers
341
"""
342
```
343
344
### Utility Classes
345
346
Helper classes for content negotiation.
347
348
```python { .api }
349
class DataAndFiles:
350
"""
351
Container for parsed multipart data.
352
"""
353
def __init__(self, data, files):
354
"""
355
Args:
356
data (dict): Form field data
357
files (dict): Uploaded file data
358
"""
359
self.data = data
360
self.files = files
361
```
362
363
## Usage Examples
364
365
### Custom Parser
366
367
```python
368
from rest_framework.parsers import BaseParser
369
from rest_framework.exceptions import ParseError
370
import yaml
371
372
class YAMLParser(BaseParser):
373
"""
374
Parser for YAML data.
375
"""
376
media_type = 'application/yaml'
377
378
def parse(self, stream, media_type=None, parser_context=None):
379
"""
380
Parse YAML data from stream.
381
"""
382
try:
383
data = stream.read()
384
return yaml.safe_load(data)
385
except yaml.YAMLError as exc:
386
raise ParseError(f'YAML parse error: {exc}')
387
388
# Usage in view
389
from rest_framework.views import APIView
390
391
class YAMLView(APIView):
392
parser_classes = [YAMLParser]
393
394
def post(self, request):
395
# request.data contains parsed YAML
396
return Response({'received': request.data})
397
```
398
399
### Custom Renderer
400
401
```python
402
from rest_framework.renderers import BaseRenderer
403
import yaml
404
405
class YAMLRenderer(BaseRenderer):
406
"""
407
Renderer for YAML output.
408
"""
409
media_type = 'application/yaml'
410
format = 'yaml'
411
412
def render(self, data, accepted_media_type=None, renderer_context=None):
413
"""
414
Render data as YAML.
415
"""
416
return yaml.dump(data, default_flow_style=False).encode('utf-8')
417
418
# Usage in view
419
class YAMLView(APIView):
420
renderer_classes = [YAMLRenderer]
421
422
def get(self, request):
423
data = {'message': 'Hello World'}
424
return Response(data)
425
```
426
427
### View-Level Parser/Renderer Configuration
428
429
```python
430
from rest_framework.parsers import JSONParser, FormParser, MultiPartParser
431
from rest_framework.renderers import JSONRenderer, TemplateHTMLRenderer
432
433
class BookView(APIView):
434
"""
435
View supporting multiple input and output formats.
436
"""
437
parser_classes = [JSONParser, FormParser, MultiPartParser]
438
renderer_classes = [JSONRenderer, TemplateHTMLRenderer]
439
440
def get(self, request):
441
books = Book.objects.all()
442
serializer = BookSerializer(books, many=True)
443
444
# Response format determined by Accept header or format suffix
445
return Response(serializer.data)
446
447
def post(self, request):
448
# Input format determined by Content-Type header
449
serializer = BookSerializer(data=request.data)
450
if serializer.is_valid():
451
serializer.save()
452
return Response(serializer.data, status=status.HTTP_201_CREATED)
453
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
454
```
455
456
### Global Parser/Renderer Configuration
457
458
```python
459
# settings.py
460
REST_FRAMEWORK = {
461
'DEFAULT_PARSER_CLASSES': [
462
'rest_framework.parsers.JSONParser',
463
'rest_framework.parsers.FormParser',
464
'rest_framework.parsers.MultiPartParser',
465
'myapp.parsers.YAMLParser', # Custom parser
466
],
467
'DEFAULT_RENDERER_CLASSES': [
468
'rest_framework.renderers.JSONRenderer',
469
'rest_framework.renderers.BrowsableAPIRenderer',
470
'myapp.renderers.YAMLRenderer', # Custom renderer
471
],
472
}
473
```
474
475
### File Upload Handling
476
477
```python
478
from rest_framework.parsers import FileUploadParser
479
from rest_framework.response import Response
480
from rest_framework import status
481
482
class FileUploadView(APIView):
483
parser_classes = [FileUploadParser]
484
485
def put(self, request, filename):
486
"""
487
Handle raw file upload.
488
"""
489
file_obj = request.data['file']
490
491
# Process the uploaded file
492
with open(f'/uploads/{filename}', 'wb') as destination:
493
for chunk in file_obj.chunks():
494
destination.write(chunk)
495
496
return Response(
497
{'message': f'File {filename} uploaded successfully'},
498
status=status.HTTP_201_CREATED
499
)
500
501
# Client usage:
502
# PUT /api/upload/document.pdf
503
# Content-Type: application/pdf
504
# Content-Disposition: attachment; filename="document.pdf"
505
# <binary file data>
506
```
507
508
### HTML Template Rendering
509
510
```python
511
class BookListView(APIView):
512
renderer_classes = [JSONRenderer, TemplateHTMLRenderer]
513
514
def get(self, request):
515
books = Book.objects.all()
516
serializer = BookSerializer(books, many=True)
517
518
# For HTML requests, provide template context
519
return Response({
520
'books': serializer.data,
521
'title': 'Book List'
522
}, template_name='books/list.html')
523
524
# books/list.html template:
525
# <h1>{{ title }}</h1>
526
# {% for book in books %}
527
# <div>{{ book.title }} by {{ book.author }}</div>
528
# {% endfor %}
529
```
530
531
### Content Negotiation Based on Accept Header
532
533
```python
534
class BookView(APIView):
535
def get(self, request):
536
books = Book.objects.all()
537
serializer = BookSerializer(books, many=True)
538
539
# Check what format was requested
540
if request.accepted_renderer.format == 'json':
541
# JSON response
542
return Response(serializer.data)
543
elif request.accepted_renderer.format == 'html':
544
# HTML response
545
return Response(
546
{'books': serializer.data},
547
template_name='books/list.html'
548
)
549
else:
550
# Default response
551
return Response(serializer.data)
552
553
# Client requests:
554
# Accept: application/json -> JSON response
555
# Accept: text/html -> HTML response
556
# Accept: */* -> Default format
557
```
558
559
### Custom Content Negotiation
560
561
```python
562
from rest_framework.content_negotiation import BaseContentNegotiation
563
564
class IgnoreClientContentNegotiation(BaseContentNegotiation):
565
"""
566
Content negotiation that ignores client preferences.
567
"""
568
def select_parser(self, request, parsers):
569
"""
570
Always select the first parser.
571
"""
572
return parsers[0]
573
574
def select_renderer(self, request, renderers, format_suffix=None):
575
"""
576
Always select JSON renderer if available.
577
"""
578
for renderer in renderers:
579
if renderer.format == 'json':
580
return (renderer, renderer.media_type)
581
582
# Fallback to first renderer
583
return (renderers[0], renderers[0].media_type)
584
585
# Usage in view
586
class CustomNegotiationView(APIView):
587
content_negotiation_class = IgnoreClientContentNegotiation
588
589
def get(self, request):
590
return Response({'message': 'Always JSON'})
591
```
592
593
### Handling Parse Errors
594
595
```python
596
from rest_framework.views import APIView
597
from rest_framework.exceptions import ParseError
598
599
class StrictJSONView(APIView):
600
parser_classes = [JSONParser]
601
602
def post(self, request):
603
try:
604
# request.data automatically parsed by JSONParser
605
data = request.data
606
607
if not isinstance(data, dict):
608
raise ParseError("Expected JSON object")
609
610
return Response({'received': data})
611
612
except ParseError as e:
613
return Response(
614
{'error': str(e)},
615
status=status.HTTP_400_BAD_REQUEST
616
)
617
```
618
619
## Utility Functions
620
621
```python { .api }
622
def zero_as_none(value):
623
"""
624
Convert zero values to None for rendering.
625
626
Args:
627
value: Value to check
628
629
Returns:
630
None if value is zero, otherwise original value
631
"""
632
```