django-ninja
django-ninja copied to clipboard
Content Negotiation
Is it possible to return a different response based on the "Accept" header or a query param e.g. ?format=json-ld
@MiltosD you can create custom renderer
overwrite render method - get Accept header and based on it value serialize response differently
The example shows that renderers are applied to the api level
api = NinjaAPI(renderer=MyRenderer())
However the case is in a @api.get() view where I check for the header and decide which renderer (better off, which Schema) to use within the view, for example, if the Accept header is "application/ld+json", then the view response is to be of type MySchemaLD, else MySchema.
I ended up with this, not sure if it's a neat approach
@api.get("/person/{pk}/", response=Any)
def get_person(request, pk, format: str = None):
person = get_object_or_404(Person, pk=pk)
by_alias = False
if format == "json-ld":
schema: Type[Schema] = PersonSchemaLD
by_alias = True
else:
schema: Type[Schema] = PersonSchema
return schema.from_orm(person).model_dump(
exclude_unset=True,
exclude_none=True,
exclude_defaults=True,
by_alias=by_alias
)
It seems to work with /person/1?format=json-ld
hard to tell - need to see how exactly your json and json-ld are different
Well, a json-ld output contains a @context object which is not present in plain json, plus information for the fields, that are relevant to rdf. For example a person might be
plain json
{
"name": "John Doe",
"country": "Germany"
}
json-ld
{
"@context": {
"ex": "http://example.com",
"dct": "http://purl.org/dc/terms/"
},
"@type": "foaf:Person",
"foaf:name": "John Doe",
"ex:country": {
"@type": "dct:Location",
"@id": "http://publications.europa.eu/resource/authority/country/DEU"
}
}
The transformation from country to ex:country is done by using serialization_alias and resolvers
We have a need to do something similar - in our case default to an application/json response but allow clients to specify text/csv using the Accept header. We've got it working nicely using a custom renderer BUT it seems that media_type is fixed per renderer. If we set it to application/json it uses that for all requests, even if we over-ride it for text/csv responses. If we leave it out at class level and try to set it dynamically per request, it always returns Content-type: None. If we return an HttpResponse with that header set explicitly it gets over-written.
Is this a bug or a feature?
FWIW happy to work on this and submit a PR for a change if it's something you'd be interested in.
@andy-isoc could you show your example of your renderer class ?
I also wanted to create a view that would return multiple content types based on Accept header. First I created a set of renderers:
class NinjaTextRenderer(NinjaBaseRenderer):
media_type = 'text/plain'
def render(self, request: HttpRequest, data: RendererContext, *, response_status: int) -> str:
...
class NinjaHtmlRenderer(NinjaTextRenderer):
media_type = 'text/html'
And a view as such
def my_view(request, ...):
...
# Do stuff
...
renderers = [NinjaTextRenderer(), NinjaHtmlRenderer()]
try:
renderer, media_type = select_renderer(request, renderers=renderers)
except ValueError:
return HTTPStatus.NOT_ACCEPTABLE, None
response = HttpResponse(
content=renderer.render(request, RendererContext(template, data.context), response_status=HTTPStatus.OK),
)
response.headers["Content-Type"] = "{}; charset={}".format(media_type, response.charset) # type: ignore[index]
return response
select_renderer is mostly a copy of rest_framework.DefaultContentNegotiation.select_renderer.
Here are links to the actual code in our app:
- Renderers and
select_renderer: https://gitlab.nic.cz/fred/django-secretary/-/blob/e74ced4f5d22c81e135fbe1ffd3afb9844f072ec/django_secretary/renderers.py#L167-289 - A view: https://gitlab.nic.cz/fred/django-secretary/-/blob/e74ced4f5d22c81e135fbe1ffd3afb9844f072ec/django_secretary/api/templates.py#L84-105