Skip to content

Request Body

Xpresso has a rich system of extractors to extract and parse request bodies. These help give you type safety, data validation and automatic OpenAPI spec generation.

We'll start off using JSON as an example since this is one of the most common types of request bodies. But as you will see in the later chapters, a lot of the same concepts apply forms and multipart requests.

Declaring a body schema

First, we need to define the schema of our body and give Xpresso a data structure to extract our body into. This data structure can be a built in type (like int or str), a collection (like dict or list) or a Pydantic model.

Info

Although Xpresso makes extensive use of Pydantic and no other similar libraries are supported out of the box, there is no reason why support could not be implemented. As you will see later, writing a custom extractor is pretty easy. It just doesn't make sense to provide out of the box integration with dozens of libraries, so we chose Pydantic.

For most use cases, you'll want to stick with a Pydantic model. Declaring a Pydantic model is simple. Start by importing BaseModel from Pydantic and declaring the fields of the model using type annotations:

from typing import Dict, Optional

from pydantic import BaseModel

from xpresso import App, FromJson, Path


class Item(BaseModel):
    name: str
    price: float
    tax: Optional[float] = None


async def create_receipt(item: FromJson[Item]) -> Dict[str, float]:
    return {item.name: item.price + (item.tax or 0)}


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

Then we declare add the FromJson[...] marker (which is syntactic sugar for Annotated[..., Json()]) to a paramter in our endpoint function:

from typing import Dict, Optional

from pydantic import BaseModel

from xpresso import App, FromJson, Path


class Item(BaseModel):
    name: str
    price: float
    tax: Optional[float] = None


async def create_receipt(item: FromJson[Item]) -> Dict[str, float]:
    return {item.name: item.price + (item.tax or 0)}


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

That's it!

Now when you receive a request it will be read as JSON and then passed to Pydantic for validation and parsing. Your function will receive a validated instance of Item.

OpenAPI and SwaggerUI

Just like path and query parameters, request bodies automatically generate OpenAPI documentation:

Swagger UI

Constraints and Customization

Pydantic supports rich validation and customization of model schemas. For in depth information on the topic, see Pydantic's docs. But here is a quick example of how this can work in Xpresso. First, import Field from Pydantic and Annotated:

from typing import Dict, Optional

from pydantic import BaseModel, Field

from xpresso import App, FromJson, Path
from xpresso.typing import Annotated


class Item(BaseModel):
    name: str
    price: Annotated[
        float,
        Field(
            gt=0,
            description="Item price without tax. Must be greater than zero.",
        ),
    ]
    tax: Optional[float] = None


async def create_receipt(item: FromJson[Item]) -> Dict[str, float]:
    return {item.name: item.price + (item.tax or 0)}


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

Tip

The import from Xpresso.typing import Annotated is just a convenience import. All it does is import Annotated from typing if your Python version is >= 3.9 and [typing_extensions] otherwise. But if you are already using Python >= 3.9, you can just replace that with from typing import Annotated.

Now use Field() inside of Annotated[...] to attach validation and schema customziation metadata to the price field:

from typing import Dict, Optional

from pydantic import BaseModel, Field

from xpresso import App, FromJson, Path
from xpresso.typing import Annotated


class Item(BaseModel):
    name: str
    price: Annotated[
        float,
        Field(
            gt=0,
            description="Item price without tax. Must be greater than zero.",
        ),
    ]
    tax: Optional[float] = None


async def create_receipt(item: FromJson[Item]) -> Dict[str, float]:
    return {item.name: item.price + (item.tax or 0)}


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

Tip

Pydantic also supports the syntax field_name: str = Field(...), but we encourage youto get used to using Annotated instead. As you will see in later chapters about forms and multipart requests, this will allow you to mix in Pydantic's validation and schema customization with Xpresso's extractor system. That said, for JSON bodies using field_name: str = Field(...) will work just fine.

Using builtin types

While you will probably need Pydantic models for complex cases, in many simple cases you can get away with just using the standard library's container types. For example, you can declare that a JSON body is a list of integers:

from typing import Dict, List

from xpresso import App, FromJson, Path


async def count_items(
    item_counts: FromJson[List[int]],
) -> Dict[str, int]:
    return {"total": sum(item_counts)}


app = App(
    routes=[
        Path(
            "/items/count",
            put=count_items,
        )
    ]
)

Mixing builtins with Pydantic

You can also wrap an existing Pydantic model in a container, for example to receive a list of items:

from typing import Dict, List, Optional

from pydantic import BaseModel

from xpresso import App, FromJson, Path


class Item(BaseModel):
    name: str
    price: float
    tax: Optional[float] = None


async def create_receipt(
    items: FromJson[List[Item]],
) -> Dict[str, float]:
    return {item.name: item.price + (item.tax or 0) for item in items}


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

Including examples

You can add examples via the examples keyword to Json(), FormData() or Multipart():

from typing import Dict, Optional

from pydantic import BaseModel

from xpresso import App, Json, Path
from xpresso.typing import Annotated


class Item(BaseModel):
    name: str
    price: float
    tax: Optional[float] = None


item_examples = {
    "With tax": Item(name="foo", price=1, tax=1),
    "Duty Free": Item(name="foo", price=2, tax=0),
}


async def create_receipt(
    item: Annotated[Item, Json(examples=item_examples)]
) -> Dict[str, float]:
    return {item.name: item.price + (item.tax or 0)}


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

The Swagger docs will now reflect this:

Swagger UI