Skip to content

Documenting OpenAPI responses

So far we have mostly just let Xpresso infer the response model from type annotation. By default, Xpresso assumes the response is JSON serializable type and uses "application/json" with an HTTP 200 status code.

You can also declare other responses or override the default response's status code, media type or schema.

Adding an error response

We can document a 404 response as follows:

from typing import Any

from pydantic import BaseModel

from xpresso import App, FromPath, Operation, Path
from xpresso.responses import JSONResponse, ResponseSpec


class Item(BaseModel):
    id: str
    value: str


class Message(BaseModel):
    message: str


async def read_item(item_id: FromPath[str]) -> Any:
    if item_id == "foo":
        return {"id": "foo", "value": "there goes my hero"}
    return JSONResponse(
        status_code=404, content={"message": "Item not found"}
    )


get_item = Operation(
    read_item,
    response_model=Item,
    responses={
        404: ResponseSpec(content={"application/json": Message}),
    },
)

app = App(routes=[Path("/items/{item_id}", get=get_item)])

Warning

Notice that when you are returning a non-default status code, you must return an actual Response, not an arbitrary JSON serializable object.

Adding media types to the default response

from typing import Any, Optional

from pydantic import BaseModel

from xpresso import App, FromPath, Operation, Path
from xpresso.parameters import FromQuery
from xpresso.responses import Response, ResponseSpec


class Item(BaseModel):
    id: str
    value: str


async def read_item(
    item_id: FromPath[str], img: FromQuery[Optional[bool]]
) -> Any:
    if img:
        return Response(b"<image bytes>", media_type="image/png")
    else:
        return {"id": "foo", "value": "there goes my hero"}


get_item = Operation(
    read_item,
    responses={
        200: ResponseSpec(
            description="OK",
            content={"application/json": Item, "image/png": bytes},
        )
    },
)

app = App(routes=[Path("/items/{item_id}", get=get_item)])

Tip

We could have also specified the return type for the 200 status code via the default_response_model parameter to Operation. Either way, we had to specify it because our function returns Any (it could return Union[dict, Response], the situation is the same) so Xpresso can't infer the right response model from type annotations.

Changing the media type for the default response

from xpresso import App, FromPath, Operation, Path, Response


async def read_item(item_id: FromPath[str]) -> bytes:
    return f"<bytes from {item_id}.png>".encode()


get_item = Operation(
    read_item,
    response_media_type="image/png",
    response_encoder=None,
    response_factory=Response,
)

app = App(routes=[Path("/items/{item_id}", get=get_item)])

Warning

Just changing the media type does not change how the response gets encoded, so we also had to pass response_encoder=None to avoid encoding bytes as JSON!

Changing the status code for the default response

from pydantic import BaseModel

from xpresso import App, Operation, Path, status


class Item(BaseModel):
    id: str
    value: str


async def create_item(item: Item) -> None:
    ...


post_item = Operation(
    create_item,
    response_status_code=status.HTTP_204_NO_CONTENT,
)

app = App(routes=[Path("/items/", post=post_item)])

Changing the default response status code via default_response_status_code changes the runtime behavior of our application in addition to the OpenAPI documentation: our endpoint will now return HTTP 201 responses.

Responses on Router and Path

You can also set responses on Router and Path, which will get merged into any responses defined for Operation. Responses for Operation take precedence over those of Path, and Path takes precedence over Routers. Status codes, media types and headers are merged. Examples and response models are overridden.

from pydantic import BaseModel

from xpresso import App, FromPath, HTTPException, Path
from xpresso.responses import ResponseSpec


class Item(BaseModel):
    id: str
    value: str


async def read_item(item_id: FromPath[str]) -> Item:
    if item_id == "foo":
        return Item(id="foo", value="there goes my hero")
    raise HTTPException(status_code=404)


app = App(
    routes=[
        Path(
            "/items/{item_id}",
            get=read_item,
            responses={
                404: ResponseSpec(
                    description="Item not found",
                )
            },
        )
    ]
)