Issue 1:
First check

I added a very descriptive title to this issue.

I used the GitHub search to find a similar issue and didn't find it.

I searched the FastAPI documentation, with the integrated search.

I already searched in Google "How to X in FastAPI" and didn't find any information.

I already read and followed all the tutorial in the docs and didn't find an answer.

I already checked if it is not related to FastAPI but to Pydantic.

I already checked if it is not related to FastAPI but to Swagger UI.

I already checked if it is not related to FastAPI but to ReDoc.

After submitting this, I commit to:

Read open issues with questions until I find 2 issues where I can help someone and add a comment to help there.

Or, I already hit the "watch" button in this repository to receive notifications and I commit to help at least 2 people that ask questions in the future.

Implement a Pull Request for a confirmed bug.

Example

I would like to be able to do this:

from fastapi import FastAPI, Query

app = FastAPI()

@app.get("/")
def get(query: Optional[str] = Query(None, alias=['q', 'query']):
return query

Description

Currently to give a query parameter multiple aliases, you need to declare multiple query parameters.

The solution you would like

Being able to give a list of aliases to the alias arg of the Query class.

Query(alias=['q', 'query'])

Of course backwards compatibility won'y break, as alias arg would be of type Optional[Union[str, Iterable[str]]].

You would still be able to do this:

Query(alias='q')

Describe alternatives you've considered

@Kludex mentioned creating a new argument aliases

Query(aliases=['q', 'query'])

Issue 2:
(Duplicate of #5390)

First Check

I added a very descriptive title to this issue.

I used the GitHub search to find a similar issue and didn't find it.

I searched the FastAPI documentation, with the integrated search.

I already searched in Google "How to X in FastAPI" and didn't find any information.

I already read and followed all the tutorial in the docs and didn't find an answer.

I already checked if it is not related to FastAPI but to Pydantic.

I already checked if it is not related to FastAPI but to Swagger UI.

I already checked if it is not related to FastAPI but to ReDoc.

Commit to Help

I commit to help with one of those options 👆

Example Code

# N/A

Description

I'd like to be able to implement a multimedia route that can optimally handle not only an entire e.g. msgpacked payload, but individual fields in the payload.

For example, if some models include bytes fields, for optimal performance, you want those fields to be (de)serialized only once, i.e. msgpack/base64 to bytes or bytes to msgpack/base64, instead of, e.g., msgpack to base64 to bytes.

Wanted Solution

Building on top of the contributions of @dmontagu, @sandys, and @jcsaaddupuy in #521, in particular this comment, I'd like for get_route_handler() to be able to pass a custom_encoder to get_request_handler(), and for get_request_handler() to be able to pass custom_encoder to serialize_response().

By being able to pass a custom msgpack encoder to get_request_handler(), you could suppress base64-encoding of custom e.g. Base64Bytes fields by overriding their encoders with lambda x: x identity passthroughs, while utilizing the existing jsonable_encoder() and custom_encoder mechanisms, without resorting to any hackery such as dynamically patching models' .__config__.json_encoders.

Wanted Code

# Copy-paste from tests/test_advanced_custom_route_class.py
# in https://github.com/natskvi/fastapi/tree/multimedia-route-support
# Requires a few more tests before it is PR-ready.

from collections import defaultdict
import msgpack
from typing import Any, Callable, Dict, Final
import pytest
import base64
import fastapi
from fastapi import APIRouter, FastAPI, status
from fastapi.routing import APIRoute, get_request_handler
from fastapi.testclient import TestClient
from pydantic import BaseModel
from starlette.requests import Request
from starlette.responses import Response
from starlette.routing import Route

app = FastAPI()

ACCEPT: Final = "accept"
CONTENT_TYPE: Final = "content-type"
JSON: Final = "application/json"
MSGPACK: Final = "application/x-msgpack"

class Base64Bytes(bytes):
"""Defines a Pydantic custom field type for deserializing base64
string to bytes."""

@classmethod
def __get_validators__(cls):
yield cls.validate

@classmethod
def validate(cls, value) -> "Base64Bytes":
if isinstance(value, str):
value = base64.b64decode(value)
return cls(value)

def base64_encode_as_str(data: bytes) -> str:
"""base64-encode binary data as ASCII string"""
return base64.b64encode(data).decode('ascii')

custom_json_encoder = {
Base64Bytes: base64_encode_as_str, # Overrides Pydantic default `lambda o: o.decode()`
}

_catchall_encoder_saved = fastapi.encoders.catchall_encoder
# fastapi.encoders.catchall_encoder callable is applied by default to obj
# (if obj is not None) after the logic that handles primitives, dicts, lists,
# tuples, Pydantic models, etc. (applying models' __config__.json_encoders,
# custom encoders, pydantic.json.ENCODERS_BY_TYPE).
fastapi.encoders.catchall_encoder = str

# Override default encoders for raw bytes fields to skip base64-encoding
# when using msgpack to serialize response payloads.
msgpack_encoder_override = defaultdict(
lambda: str,
{
Base64Bytes: lambda x: x, # Overrides custom_json_encoder's `base64_encode_as_str`
},
)

class MsgpackRequest(Request):
"""Msgpack request class, used only if the request headers include
'Content-Type: application/x-msgpack'."""

media_type = MSGPACK

async def body(self) -> bytes:
if not hasattr(self, "_body"):
body: Final = await super().body()
if body:
# fastapi.tiangolo.com/advanced/custom-request-and-route/#handling-custom-request-body-encodings
self._body = msgpack.unpackb(body)
return self._body

class MsgpackResponse(Response):
"""Msgpack response class, used only if the request headers include
'Accept: application/x-msgpack'."""

media_type = MSGPACK

def render(self, content: Any) -> bytes:
return msgpack.packb(content)

class MultimediaRouteMixin(APIRoute):
"""A route "mixin" that handles 'application/x-msgpack' in addition to
'application/json'. Its generic design simplifies adding support for more
mime types. (Hence, a more accurate name for this class would be
`MultiMimeTypeRouteMixin`.)
Credit:
https://github.com/tiangolo/fastapi/issues/521#issuecomment-646989249"""

REQUEST_CLASSES: Final = {
MsgpackRequest.media_type: MsgpackRequest,
}

RESPONSE_CLASSES: Final = {
MsgpackResponse.media_type: MsgpackResponse,
}

def _get_multimedia_request_handler(self, mime_type):
# Use custom response class or fall back on default self.response_class.
response_class = self.RESPONSE_CLASSES.get(mime_type, self.response_class)
# Use msgpack override encoder for msgpack responses.
custom_encoder = msgpack_encoder_override if mime_type == MSGPACK else None

return get_request_handler(
dependant=self.dependant,
body_field=self.body_field,
status_code=self.status_code,
response_class=response_class, # Based on mime_type (see above)
response_field=self.secure_cloned_response_field,
response_model_include=self.response_model_include,
response_model_exclude=self.response_model_exclude,
response_model_by_alias=self.response_model_by_alias,
response_model_exclude_unset=self.response_model_exclude_unset,
response_model_exclude_defaults=self.response_model_exclude_defaults,
response_model_exclude_none=self.response_model_exclude_none,
dependency_overrides_provider=self.dependency_overrides_provider,
custom_encoder=custom_encoder, # Based on mime_type (see above)
)

def get_route_handler(self) -> Callable:
"""Overrides APIRoute.get_route_handler()"""

async def multimedia_route_handler(request: Request) -> Response:

content_type_headers = request.headers.get("Content-Type")
try:
request_cls = self.REQUEST_CLASSES[content_type_headers]
request = request_cls(request.scope, request.receive)
except KeyError:
pass # No handler for content_type, process request as-is.

accept_headers = request.headers.get("Accept")
route_handler = self._get_multimedia_request_handler(accept_headers)
return await route_handler(request)

return multimedia_route_handler

class ModelConfig:
json_encoders = custom_json_encoder
validate_assignment = True

class ModelWithBytes(BaseModel):
text: str
raw: Base64Bytes

class Config(ModelConfig):
pass

TEXT: Final ="text"
BYTES: Final = b"\x16\r\xd4)\x93>(\x04\x83\xcc\xc5\xd6"

test_dict = {
"text": TEXT,
"raw": BYTES,
}

base64_encoded_test_dict = {
"text": TEXT,
"raw": base64_encode_as_str(BYTES),
}

router = APIRouter(route_class=MultimediaRouteMixin)

@router.post("")
def post_test_model(body: ModelWithBytes) -> ModelWithBytes:
assert body == ModelWithBytes(text=TEXT, raw=BYTES)
assert ModelConfig.json_encoders[Base64Bytes] is base64_encode_as_str
# Verify jsonable_encoder did not mutate body
# TODO: assert Base64Bytes not in body.__config__.json_encoders
return body

ENDPOINT_PATH: Final = "/test_multimedia"

app.include_router(router=router, prefix=ENDPOINT_PATH)

client = TestClient(app)

def test_multimedia_route():
"""Test multimedia "middleware" (route mixin) for a simple payload
that includes raw bytes for two combinations of 'Accept' and
'Content-Type' HTTP headers."""

# Test JSON-to-JSON POST
for headers in (
{},
{ACCEPT: JSON},
{CONTENT_TYPE: JSON},
{ACCEPT: JSON, CONTENT_TYPE: JSON},
):
resp = client.post(
ENDPOINT_PATH,
json=base64_encoded_test_dict,
headers=headers,
)
assert resp.ok
result = resp.json()
assert resp.status_code == status.HTTP_200_OK
assert result == base64_encoded_test_dict

# Test JSON-to-msgpack POST
for headers in (
{ACCEPT: MSGPACK},
{ACCEPT: MSGPACK, CONTENT_TYPE: JSON},
):
resp = client.post(
ENDPOINT_PATH,
headers=headers,
json=base64_encoded_test_dict,
)
assert resp.ok
assert resp.status_code == status.HTTP_200_OK
result = msgpack.unpackb(resp.content)
assert result == test_dict

# Test msgpack-to-JSON POST
for headers in (
{CONTENT_TYPE: MSGPACK},
{ACCEPT: JSON, CONTENT_TYPE: MSGPACK},
):
resp = client.post(
ENDPOINT_PATH,
headers=headers,
data=msgpack.packb(test_dict),
)
assert resp.ok
result = resp.json()
assert resp.status_code == status.HTTP_200_OK
assert result == base64_encoded_test_dict

# Test msgpack-to-msgpack POST
for headers in (
{ACCEPT: MSGPACK, CONTENT_TYPE: MSGPACK},
):
resp = client.post(
ENDPOINT_PATH,
headers=headers,
data=msgpack.packb(test_dict),
)
assert resp.ok
assert resp.status_code == status.HTTP_200_OK
result = msgpack.unpackb(resp.content)
assert result == test_dict

# Test for error when sending JSON but expecting msgpack
resp = client.post(
ENDPOINT_PATH,
headers={CONTENT_TYPE: MSGPACK},
json=base64_encoded_test_dict,
)
assert not resp.ok
assert resp.status_code == status.HTTP_400_BAD_REQUEST

# Test for error when sending msgpack but expecting JSON
resp = client.post(
ENDPOINT_PATH,
headers={CONTENT_TYPE: JSON},
data=msgpack.packb(test_dict),
)
assert not resp.ok
assert resp.status_code == status.HTTP_400_BAD_REQUEST

# Restore pre-test state
fastapi.encoders.catchall_encoder = _catchall_encoder_saved

Alternatives

No response

Operating System

Linux

Operating System Details

Ubuntu 20.04.5 LTS

FastAPI Version

0.83.0

Python Version

3.8.12

Additional Context

closes #5389

closes #5390

#521

#2117

#2901