In one of 3.0 wishlist points @rob
suggested to support multiple resource representation in response based on Accept
header https://discord.com/channels/919193495116337154/1182745702942646313/1217489384669184064
I've decided to write few words about current situation, problems, possible interface for this (possible) future feature.
In the current Litestar 2.x version if we want to return different resource representation based on Accept
header we have to it manually using "ifology" which in case of many possible representations (json, xml, csv, yaml...) is not the most elegant solution. This can also lead to code duplication if we have more endpoints.
Simple example can look like this:
from dataclasses import asdict, dataclass
from typing import Annotated
from litestar import Litestar, get
from litestar.enums import MediaType
from litestar.params import Parameter
from litestar.response import Response
from litestar.status_codes import HTTP_406_NOT_ACCEPTABLE
@dataclass
class User:
name: str
age: int
email: str
def convert_user_to_yaml(user: User) -> bytes: ...
def convert_user_to_xml(user: User) -> bytes: ...
def convert_user_to_csv(user: User) -> bytes: ...
@get
def get_user(accept: Annotated[str, Parameter(header="Accept")]) -> User:
user = User(name="Olga", age=29, email="[email protected]")
if accept == "application/x-yaml":
return Response(content=convert_user_to_yaml(user), media_type=accept)
if accept == "application/xml":
return Response(content=convert_user_to_xml(user), media_type=accept)
if accept == "text/csv":
return Response(content=convert_user_to_csv(user), media_type=accept)
if accept in {"application/json", "*/*"}:
return Response(content=asdict(user), media_type=MediaType.JSON)
raise Response(status_code=HTTP_406_NOT_ACCEPTABLE)
app = Litestar([get_user])
Sure we can write custom methods and extend base data model classes (pydantic, msgspec, attrs...) but implementing logic for every model type with including/excluding format mechanisms may be complex and time consuming. And we still need to figure out how to handle different responses for DTOs.
Customizable, extendable and easy to use resource representation response based on Accept
header can be nice addition to the Litestar. At least I think so :)
Accept
header is set to "*/*"
if request does not provide any (https://github.com/litestar-org/litestar/blob/main/litestar/connection/request.py#L120)media_type
is set on route handler level and passed to Response.render
method ("application/json"
by default)Response.render
matches media_type
patterns and renders properly encoded messageI like the way how the DRF handle this https://www.django-rest-framework.org/api-guide/renderers/
What if we can "borrow" their idea?
There is obstacle. We have media_type
in route handler definition.
But we can move media_type
argument Response
object, right?
Take a look at this simpliefied example:
class YamlResponse(Response):
media_type = "application/x-yaml"
def render(self, content: Any, enc_hook: Serializer = default_serializer) -> bytes:
return encode_yaml(content, enc_hook)
class XmlResponse(Response):
media_type = "application/xml"
def render(self, content: Any, enc_hook: Serializer = default_serializer) -> bytes:
return encode_xml(content, enc_hook)
class CsvResponse(Response):
media_type = "text/csv"
def render(self, content: Any, enc_hook: Serializer = default_serializer) -> bytes:
return encode_csv(content, enc_hook)
class JsonResponse(Response):
media_type = "application/json"
def render(self, content: Any, enc_hook: Serializer = default_serializer) -> bytes:
return encode_json(content, enc_hook)
# NOTE: response_class renamed to response_classes
@get(response_classes=[JsonResponse, YamlResponse, CsvResponse, XmlResponse])
def user() -> User:
return User(name="Olga", age=29, email="[email protected]")
It looks nice.
Simple and reusable.
Supports DTOs.
media_type
can be fixed content type, regex or */*
There should be negotiation between client and server to check if Accept
header is supported in endpoint.
In get_response_handler
we can iterate over response_classes
and check if any of them supports specified Accept
format.
First matched class will be used as response_class
in later steps. If none of them will match then HTTP_406_NOT_ACCEPTABLE
can be returned by default (or not).
Response
classesmedia_type
in handler definitionPay now to fund the work behind this issue.
Get updates on progress being made.
Maintainer is rewarded once the issue is completed.
You're funding impactful open source efforts
You want to contribute to this effort
You want to get funding like this too