Skip to content

Forms

To extract forms in Xpresso, you start by declaring a data structure to unpack the form into. The fields of the datastructure correspond to the fields of the form data. The datastructure can be almost anything, including dataclasses, pydantic models and regular Python classes.

from dataclasses import dataclass
from typing import List

from pydantic import BaseModel

from xpresso import (
    App,
    ExtractField,
    FromFormData,
    FromFormField,
    FromJson,
    Path,
)


class JsonModel(BaseModel):
    foo: str


@dataclass(frozen=True)
class FormDataModel:
    name: str  # implicit FromFormField[str]
    tags: FromFormField[List[str]]
    json_data: ExtractField[FromJson[JsonModel]]


async def compare_json_to_form(
    form: FromFormData[FormDataModel],
) -> bool:
    return form.json_data.foo in form.tags


app = App(routes=[Path(path="/form", post=compare_json_to_form)])

This request extracts a application/x-www-form-urlencoded request into a FormDataModel object.

Note

Form fields (FromFormField) are extracted from the form directly, but things like JSON or files need to be yanked out of a specific field before they are parsed/extracted. So for non form-native fields (anything except FromFormField) you need to wrap it in ExtractField.

Form serialization

Xpresso fully supports the OpenAPI parameter serialization standard. You can customize how extraction ocurrs using the style and explode keyword arguments to FormField():

from dataclasses import dataclass
from typing import List

from xpresso import App, FormEncodedField, FromFormData, Path
from xpresso.typing import Annotated


@dataclass(frozen=True)
class FormDataModel:
    tags: Annotated[
        List[str],
        FormEncodedField(style="spaceDelimited", explode=False),
    ]


async def echo_tags(form: FromFormData[FormDataModel]) -> List[str]:
    return form.tags  # returned as JSON


app = App(routes=[Path(path="/echo-tags", post=echo_tags)])

Multipart requests

Multipart requests (multipart/form-data) can be parsed almost identically to application/x-www-form-urlencoded. You can't upload mixed files and data in an application/x-www-form-urlencoded request, so you'll need to use a Multipart request. Multipart requests even support multiple files:

from dataclasses import dataclass
from typing import List

from pydantic import BaseModel

from xpresso import (
    App,
    ExtractRepeatedField,
    FromFile,
    FromFormField,
    FromMultipart,
    Path,
    UploadFile,
)


class JsonModel(BaseModel):
    foo: str


@dataclass(frozen=True)
class FormDataModel:
    name: str  # implicit FromFormField[str]
    tags: FromFormField[List[str]]
    files: ExtractRepeatedField[FromFile[List[UploadFile]]]


class UserUploadMetadata(BaseModel):
    name: str
    tags: List[str]
    nbytes: int


async def upload_data(
    form: FromMultipart[FormDataModel],
) -> UserUploadMetadata:
    nbytes = 0
    for file in form.files:
        nbytes += len(await file.read())
    return UserUploadMetadata(
        name=form.name,
        tags=form.tags,
        nbytes=nbytes,
    )


app = App(routes=[Path(path="/form", post=upload_data)])

Note

Fields in a application/x-www-form-urlencoded or multipart/form-data request can be repeated. This just means that a field of the same name appears more than once in the request. Often this is used to upload multiple files, such as in the example above. To declare repeated fields we need to do two things: 1. Use ExtractRepeatedField instead of ExtractField. 1. Make sure our type is actually a List/Tuple/Set (any sequence will do).