Renderers¶
Every response Django Ninja sends — including the ones it generates itself,
like validation errors — is turned into bytes by a single renderer
attached to the NinjaAPI instance. By default that's JSON, but you can
swap in anything else: a different JSON library, XML, CSV, or a custom
media type of your own.
Basic usage¶
Pass a renderer instance when creating the API:
from ninja import NinjaAPI
from ninja.renderers import BaseRenderer
class TextRenderer(BaseRenderer):
media_type = "text/plain"
def render(self, request, data, *, response_status):
return str(data)
api = NinjaAPI(renderer=TextRenderer())
The renderer is set once, on the NinjaAPI instance, and applies to every
operation registered on it — there's no per-router or per-operation
override.
Writing a custom renderer¶
Subclass ninja.renderers.BaseRenderer and define:
media_type— the media type used for the response'sContent-Typeheader, e.g."application/xml".charset— defaults to"utf-8". Combined withmedia_typeto build the full header, e.g.application/xml; charset=utf-8.render(self, request, data, *, response_status)— receives theHttpRequest, the object returned by your view (already validated and shaped by anyresponse=Schemayou declared), and theintHTTP status code. Return astrorbytesvalue — that's exactly what goes on the wire as the response body.
Warning
Because Django Ninja's built-in error responses (validation errors,
HttpError, 404s) are serialized through the same renderer as your own
views, render needs to handle plain dict payloads too — not just the
shapes your own schemas produce. The XML and CSV examples below only
work for the data shapes they were written for; a production renderer
should be more defensive.
The default JSON renderer¶
ninja.renderers.JSONRenderer is what you get when you don't pass
renderer at all. It serializes with the standard library's json module
and two attributes you can override:
encoder_class— defaults toninja.responses.NinjaJSONEncoder, which extends Django'sDjangoJSONEncoder(so it already handlesdatetime,date,Decimal,UUID, etc.) and additionally knows how to serialize Pydantic models (via.model_dump()),AnyUrl/Urlvalues,IPv4Address/IPv6Address/IPv4Network/IPv6Network, andEnummembers.json_dumps_params— adictof extra keyword arguments passed straight tojson.dumps(...), e.g.{"indent": 2}or{"sort_keys": True}.
Subclassing JSONRenderer lets you tweak either without reimplementing
render:
from ninja import NinjaAPI
from ninja.renderers import JSONRenderer
class PrettyJSONRenderer(JSONRenderer):
json_dumps_params = {"indent": 2}
api = NinjaAPI(renderer=PrettyJSONRenderer())
Example: a faster JSON renderer¶
orjson is a fast JSON library that also
serializes dataclass, datetime, numpy and UUID instances natively.
Swapping it in only takes a few lines:
import orjson
from ninja import NinjaAPI
from ninja.renderers import BaseRenderer
class ORJSONRenderer(BaseRenderer):
media_type = "application/json"
def render(self, request, data, *, response_status):
return orjson.dumps(data)
api = NinjaAPI(renderer=ORJSONRenderer())
Tip
orjson.dumps returns bytes, not str — that's fine, Django's
HttpResponse accepts bytes content directly.
Example: an XML renderer¶
from io import StringIO
from django.utils.encoding import force_str
from django.utils.xmlutils import SimplerXMLGenerator
from ninja import NinjaAPI
from ninja.renderers import BaseRenderer
class XMLRenderer(BaseRenderer):
media_type = "text/xml"
def render(self, request, data, *, response_status):
stream = StringIO()
xml = SimplerXMLGenerator(stream, "utf-8")
xml.startDocument()
xml.startElement("data", {})
self._to_xml(xml, data)
xml.endElement("data")
xml.endDocument()
return stream.getvalue()
def _to_xml(self, xml, data):
if isinstance(data, (list, tuple)):
for item in data:
xml.startElement("item", {})
self._to_xml(xml, item)
xml.endElement("item")
elif isinstance(data, dict):
for key, value in data.items():
xml.startElement(key, {})
self._to_xml(xml, value)
xml.endElement(key)
elif data is not None:
xml.characters(force_str(data))
api = NinjaAPI(renderer=XMLRenderer())
This code is based on DRF-xml.
Example: a CSV renderer¶
A renderer that only needs to handle one specific, known shape — a list of flat dicts with identical keys — can be much shorter:
from ninja.renderers import BaseRenderer
class CSVRenderer(BaseRenderer):
media_type = "text/csv"
def render(self, request, data, *, response_status):
rows = [",".join(data[0].keys())]
rows += [",".join(map(str, item.values())) for item in data]
return "\n".join(rows)
Edge cases and tips¶
- A renderer instance is shared across every request; keep it stateless
(all the built-in and example renderers above only read
data, never store anything onself). get_content_type()onNinjaAPIbuilds the response'sContent-Typeheader asf"{renderer.media_type}; charset={renderer.charset}"— the; charset=...part is always appended, so settingcharset = ""on your renderer only leaves you with a trailingcharset=, not a bare media type. If you need the header to be exactlymedia_typewith no charset parameter at all, overrideget_content_type()on aNinjaAPIsubclass.- Renderers are orthogonal to request parsers: a parser controls how the incoming request body is read, a renderer controls how the outgoing response body is written. You can mix and match freely, e.g. accept JSON bodies but render XML responses.