or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

auth-permissions.mdcontent-negotiation.mddecorators.mdfields-validation.mdgeneric-views.mdindex.mdpagination-filtering.mdrequest-response.mdrouters-urls.mdserializers.mdstatus-exceptions.mdtesting.mdviews-viewsets.md

content-negotiation.mddocs/

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

```