# How To Generate an OpenAPI Document With FastAPI

# How to generate an OpenAPI document with FastAPI

<div className="mt-10 flex items-center justify-center">

</div>

Many developers start their API development with FastAPI, and with good reason. FastAPI has rapidly gained traction in the Python community for its excellent performance, intuitive design, and flexibility. It enables developers to craft API solutions that not only run fast but also meet their users' unique needs.

FastAPI is great for building your core API, but you'll want to layer on SDKs and docs to provide your users with easy integration. For that, you'll want an OpenAPI document.

The good news is that FastAPI provides you with an OpenAPI document out of the box. The less good news is that you'll need some tweaking to get the OpenAPI document to a level where it becomes usable with other tooling.

This article will show you how to improve the default OpenAPI document generation to make the most of the generated schema.

## Generating an OpenAPI document with FastAPI

Understanding how FastAPI generates OpenAPI documents can help you make more informed decisions when you customize your FastAPI setup.

The process is fairly straightforward: FastAPI builds the OpenAPI document based on the routes and models you've defined in your application. For every route in your FastAPI application, FastAPI adds an operation to the OpenAPI document. For every model used in these routes, FastAPI adds a schema definition. The request and response bodies, parameters, and headers all draw from these schema definitions.

While this process works well out of the box, FastAPI also offers several customization options that can change the generated OpenAPI document. We'll cover some of these options in the following sections.

## Our FastAPI example app: APItizing Burgers

Let's get this out of the way: The name came in a daydream shortly before lunchtime.

To guide us through this journey, we'll use a simple example FastAPI application: the "APItizing Burgers" burger shop API. This API includes two models, `Burger` and `Order`, and provides basic CRUD operations for managing burgers and orders at our hypothetical burger shop. Additionally, we have a webhook defined for order status events.

We'll look at how we optimized this FastAPI application and refined our models and routes so that the generated OpenAPI document is intuitive and easy to use. We will also explore how we can use this schema to generate SDKs using Speakeasy. The source code for our example API is available in the [framework-fastapi](https://github.com/speakeasy-api/examples/tree/main/framework-fastapi) folder of the Speakeasy Examples repository.

The `framework-fastapi` directory consists of two directories: `app` and `sdk`.

The `app` directory contains the FastAPI server definition: `app/main.py`. This is where we'll look at what we customized.

The `sdk` directory and the two OpenAPI documents, `openapi.yaml` and `openapi.json`, are generated by running `gen.sh` in the root of the project.

Join us as we dive into FastAPI customization and discover how these tweaks can streamline your SDK generation process.

<Callout title="Warning" type="warning">
  When using Pydantic to define models, a known issue is that the serialization
  of `datetime` objects is not timezone-aware. This will cause a mismatch with
  the OpenAPI format `date-time`, which requires RFC 3339 date-time strings with
  timezones included. Consider using
  [`AwareDatetime`](https://docs.pydantic.dev/2.5/api/types/#pydantic.types.AwareDatetime)
  fields in Pydantic models to enable the appropriate
  [validation](https://docs.pydantic.dev/latest/errors/validation_errors/#timezone_aware)
  and ensure your SDK behavior matches the response definition from your server.
</Callout>

## Basic FastAPI setup

Let's get started with the basics – some things you probably do already.

These straightforward examples are trivial but will help you better understand the three steps in the automation pipeline: How FastAPI setup influences OpenAPI documents, which, in turn, influences SDK code.

### Scalar API documentation

FastAPI automatically generates API documentation using Swagger UI. This example also has [Scalar](https://scalar.com/) API documentation. Scalar is an alternative to Swagger UI. Its standout feature, as explained in our blog post, [Choosing a docs vendor: Mintlify vs Scalar vs Bump vs ReadMe vs Redocly](https://www.speakeasy.com/blog/choosing-a-docs-vendor#mintlify-seriously-beautiful-docs), is that it can be used as a standalone API client. Scalar is also easy to use and has a modern UI that can be customized to create branded documentation. We use it for our [Speakeasy docs](https://www.speakeasy.com/blog/release-speakeasy-docs#why-we-partnered-with-scalar).

We added Scalar to the FastAPI app by installing the [Scalar FastAPI plugin](https://github.com/scalar/scalar/blob/main/integrations/fastapi/README.md).

After installing the plugin, we added a "/scalar" route, in `app/main.py`, that returns the Scalar API Reference:

```python
from scalar_fastapi import get_scalar_api_reference

# ...

app = FastAPI()

# ...

@app.get("/scalar", include_in_schema=False)
async def scalar_html():
    return get_scalar_api_reference(
        openapi_url=app.openapi_url,
        title=app.title + " - Scalar",
    )

```

### Server configuration

This may seem obvious, but while first working with FastAPI in development, the generated docs, development server, and API operations all work out of the box without the need to manually specify your server address.

However, when generating SDKs, your OpenAPI document needs to list servers.

In our `app/main.py`, we added our local server as shown:

```python
from fastapi import FastAPI

app = FastAPI(
    servers=[
        {"url": "http://127.0.0.1:8000", "description": "Local server"},
    ],
)
```

This leads to the following generated output in `openapi.yaml`:

```yaml
# The basic server configuration in OpenAPI
servers:
  - description: Local server
    url: http://127.0.0.1:8000/
  # You can add additional servers if needed
  # - description: Production server
  #   url: https://api.example.com/
```

### Application information

In our `app/main.py`, if we have the following:

```python
from fastapi import FastAPI

app = FastAPI(
    summary="A simple API to manage burgers and orders",
    description="This API is used to manage burgers and orders in a restaurant",
    version="0.1.0",
    title="APItizing Burger API",
)
```

FastAPI generates the following YAML in our `openapi.yaml` file:

```yaml
info:
  description: This API is used to manage burgers and orders in a restaurant
  summary: A simple API to manage burgers and orders
  title: APItizing Burger API
  version: 0.1.0
```

### Route customizations

With the basics out of the way, let's look at a few more substantial recommendations.

### Typed responses

When developers use your generated SDK, they may wish to see what all the possible responses for an API call could be.

With FastAPI, you can add additional responses to each route by specifying a response type.

In our `app/main.py`, we added this abbreviated code:

```python
from fastapi import FastAPI
from fastapi.responses import JSONResponse
from pydantic import BaseModel, Field

class ResponseMessage(BaseModel):
    """A response message"""

    message: str = Field(description="The response message")

OPENAPI_RESPONSE_BURGER_NOT_FOUND = {
    "model": ResponseMessage,
    "description": "Burger not found",
}

def response_burger_not_found(burger_id: int):
    """Response for burger not found"""

    return JSONResponse(
        status_code=404,
        content=f"Burger with id {burger_id} does not exist",
    )

class Burger(BaseModel):
    id: int
    name: str
    description: str = None

app = FastAPI()

@app.get(
    "/burger/{burger_id}",
    response_model=BurgerOutput,
    responses={404: OPENAPI_RESPONSE_BURGER_NOT_FOUND},
    tags=["burger"],
)
def read_burger(burger_id: Annotated[int, Path(title="Burger ID")]):
    """Read a burger"""

    for burger in burgers_db:
        if burger.id == burger_id:
            return burger
    return response_burger_not_found(burger_id)
```

FastAPI adds a schema for our specific error message to `openapi.yaml`:

```yaml
components:
  schemas:
    ResponseMessage:
      description: A response message
      properties:
        message:
          description: The response message
          title: Message
          type: string
      required:
        - message
      title: ResponseMessage
      type: object
```

### Operation tags

As your API develops and grows bigger, you're likely to split it into separate files. FastAPI [provides conveniences](https://fastapi.tiangolo.com/tutorial/bigger-applications/) to help reduce boilerplate and repetition when splitting an API into multiple modules.

While this separation may reduce cognitive overhead while you're working in particular sections of the API code, it doesn't mean similar groups are automatically created in your documentation and SDK code.

We recommend you add tags to all operations in FastAPI, whether you're building a big application or only have a handful of operations, so that operations can be grouped by tag in generated SDK code and documentation.

```yaml filename="operation-tags.yaml"
# Tags help organize operations into logical groups
tags:
  - name: burger
  - name: order
```

The most straightforward way to add tags is to edit each operation and add a list of tags. This example highlights the tags list:

```python
from fastapi import FastAPI

app = FastAPI()

@app.get(
    "/burger/{burger_id}",
    tags=["burger"],
)
def read_burger(burger_id: int):
    return {
        "burger_id": burger_id,
    }
```

### Tag metadata

You can add metadata to your tags to further improve the developer experience.

FastAPI accepts a parameter called `openapi_tags`, which we can use to add metadata, such as a description and a list of external documentation links.

Here's how to add metadata to tags:

```python
from fastapi import FastAPI

tags_metadata = [
    {
        "name": "burger",
        "description": "Operations related to burgers",
        "externalDocs": {
            "description": "Burger external docs",
            "url": "https://en.wikipedia.org/wiki/Hamburger",
        },
    },
    {
        "name": "order",
        "description": "Operations related to orders",
    },
]

app = FastAPI(
    openapi_tags=tags_metadata,
)

@app.get(
    "/burger/{burger_id}",
    tags=["burger"],
)
def read_burger(burger_id: int):
    return {
        "burger_id": burger_id,
    }
```

When we add metadata to tags, FastAPI adds a top-level `tags` section to our OpenAPI document:

```yaml
tags:
  - description: Operations related to burgers
    externalDocs:
      description: Burger external docs
      url: https://en.wikipedia.org/wiki/Hamburger
    name: burger
  - description: Operations related to orders
    name: order
```

Each tagged path in our OpenAPI document also gets a list of tags:

```yaml
paths:
  /burger/{burger_id}:
    get:
      description: Read a burger
      operationId: readBurger
      summary: Read Burger
      tags:
        - burger
      # ...
```

### Operation ID customization

When FastAPI outputs an OpenAPI document, it generates a unique OpenAPI `operationId` for each path. By default, this unique ID is generated by the FastAPI `generate_unique_id` function:

```python
def generate_unique_id(route: "APIRoute") -> str:
    operation_id = route.name + route.path_format
    operation_id = re.sub(r"\W", "_", operation_id)
    assert route.methods
    operation_id = operation_id + "_" + list(route.methods)[0].lower()
    return operation_id
```

This can often lead to cumbersome and unintuitive names. To improve usability, we have two methods of customizing these generated strings:

1. Using a custom `generate_unique_id_function`
2. Specifying `operation_id` per operation

The preferred method is to use a custom function when you generate unique IDs for paths.

The example below is an illustrative function that doesn't generate guaranteed-unique IDs and doesn't handle method names without an underscore. However, it demonstrates how you can add a function that generates IDs based on an operation's method name:

```python
from fastapi import FastAPI

def convert_snake_case_to_camel_case(string: str) -> str:
    """Convert snake case to camel case"""

    words = string.split("_")
    return words[0] + "".join(word.title() for word in words[1:])

def custom_generate_unique_id_function(route: APIRoute) -> str:
    """Custom function to generate unique id for each endpoint"""

    return convert_snake_case_to_camel_case(route.name)

app = FastAPI(
    generate_unique_id_function=custom_generate_unique_id_function,
)
```

With FastAPI, you can also specify the `operationId` per operation. For our example, we'll add a new parameter called `operation_id` to the operation decorator:

```python
from fastapi import FastAPI

app = FastAPI()

@app.get(
    "/burger/{burger_id}",
    operation_id="readBurger",
)
def read_burger(burger_id: int):
    pass
```

### Webhooks

Starting with OpenAPI version 3.1.0, it is possible to specify webhooks for your application in OpenAPI.

Here's how to add a webhook to FastAPI:

```python
from fastapi import FastAPI

app = FastAPI()

class Order(BaseModel):
    id: int
    note: str

@app.webhooks.post(
    "order-status-changed",
    operation_id="webhookOrderStatusChanged",
)
def webhook_order_status_changed(body: Order):
    """
    When an order status is changed, this webhook will be triggered.

    The server will send a `POST` request with the order details to the webhook URL.
    """
    pass
```

FastAPI generates the following top-level `webhooks` section in `openapi.yaml`:

```yaml
webhooks:
  order-status-changed:
    post:
      description:
        "When an order status is changed, this webhook will be triggered.

        The server will send a `POST` request with the order details to the webhook
        URL."
      operationId: webhookOrderStatusChanged
      requestBody:
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/Order"
        required: true
      responses:
        "200":
          content:
            application/json:
              schema: {}
          description: Successful Response
        "422":
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HTTPValidationError"
          description: Validation Error
      summary: Webhook Order Status Changed
```

## Speakeasy integration

Now that we have a customized OpenAPI document, we can use Speakeasy to generate SDKs based on it. You can generate an SDK by running the `gen.sh` bash script, which runs the `speakeasy generate` command. Alternatively, you can use the Speakeasy quick start command for a guided SDK setup:

```bash filename="Terminal"
speakeasy quickstart
```

Following the prompts, provide the OpenAPI document location, name your SDK, and select the SDK language.
In the terminal, you'll see the steps taken by Speakeasy to create the SDK:

```
│   └─Source: APItizing Burgers API - success
│     └─Validating Document - success
│     └─Diagnosing OpenAPI - success
│     └─Tracking OpenAPI Changes - success
│       └─Snapshotting OpenAPI Revision - success
│       └─Storing OpenAPI Revision - success
│   └─Validating gen.yaml - success
│   └─Generating Python SDK - success
│     └─Setup Environment - success
│     └─Load and Validate Document - success
│     └─Generate SDK - success
│     └─Compile SDK - success
│     └─Setup Environment - success
│     └─Load and Validate Document - success
│     └─Generate SDK - success
│   └─Generating Code Samples - success
│     └─Snapshotting Code Samples - success
│       └─Snapshotting Code Samples - success
│       └─Uploading Code Samples - success
```

Speakeasy [validates](/docs/concepts#validation) the OpenAPI document to check that it's ready for code generation. Validation issues will be printed in the terminal and the generated SDK will be saved as a folder in your project.
Speakeasy also suggests improvements for your SDK using [Speakeasy Suggest](/docs/prep-openapi/maintenance), which is an AI-powered tool in Speakeasy Studio. You can see suggestions by opening the link to your Speakeasy Studio workspace in the terminal.

Let's take a look at how the information we detailed in the OpenAPI document affects how Speakeasy generates SDKs.

Adding the local server information leads to the following generated output in the `openapi.yaml` file:

```yaml
# This server configuration will be used by Speakeasy when generating SDKs
openapi: 3.1.0
info:
  title: APItizing Burgers API
  version: 0.1.0
servers:
  - description: Local server
    url: http://127.0.0.1:8000/
```

After Speakeasy generates the SDK, this leads to the following abbreviated code in `sdk/src/openapi/sdkconfiguration.py`:

```python
from dataclasses import dataclass

SERVERS = [
    'http://127.0.0.1:8000/',
    # Local server
]
"""Contains the list of servers available to the SDK"""

@dataclass
class SDKConfiguration:
    ...
    server_url: Optional[str] = ""
    server_idx: Optional[int] = 0
    ...

    def __post_init__(self):
        self._hooks = SDKHooks()

    def get_server_details(self) -> Tuple[str, Dict[str, str]]:
        if self.server_url is not None and self.server_url:
            return remove_suffix(self.server_url, "/"), {}
        if self.server_idx is None:
            self.server_idx = 0

        return SERVERS[self.server_idx], {}
```

You'll find calls to `SDKConfiguration.get_server_details()` when the SDK builds API URLs:

```python
from dataclasses import dataclass

SERVERS = [
    'http://127.0.0.1:8000/',
    # Local server
]
"""Contains the list of servers available to the SDK"""

@dataclass
class SDKConfiguration:
    ...
    server_url: Optional[str] = ""
    server_idx: Optional[int] = 0
    ...

    def __post_init__(self):
        self._hooks = SDKHooks()

    def get_server_details(self) -> Tuple[str, Dict[str, str]]:
        if self.server_url is not None and self.server_url:
            return remove_suffix(self.server_url, "/"), {}
        if self.server_idx is None:
            self.server_idx = 0

        return SERVERS[self.server_idx], {}
```

Speakeasy uses the title, summary, and descriptions we provided earlier to add helpful text to the generated SDK documentation, including comments in the SDK code. For example, in `sdk/src/openapi/sdk.py`:

```python
class SDK(BaseSDK):
    r"""APItizing Burgers API: A simple API to manage burgers and orders

    This API is used to manage burgers and orders in a restaurant
    """
```

Speakeasy adds the version to the `SDKConfiguration` in `sdk/src/openapi/sdkconfiguration.py`. It also uses this version to construct the user agent (`user_agent`), which contains the version of the SDK, the version of the Speakeasy generator build, and the version of the OpenAPI documentation:

```python
from dataclasses import dataclass

@dataclass
class SDKConfiguration:
    ...
    openapi_doc_version: str = '0.1.0'
    user_agent: str = "speakeasy-sdk/python 0.2.0 2.607.0 0.1.0 openapi"
    ...
```

When users call your API using the generated SDK, the `user_agent` from `SDKConfiguration` is automatically added to the `user-agent` header. The `_build_request_with_client` method in `BaseSDK` constructs the HTTP request and sets the header using `headers[user_agent_header] = self.sdk_configuration.user_agent`:

```python
def _build_request_with_client(
    self,
    ...
    user_agent_header,
    ...
) -> httpx.Request:
    ...
    headers["Accept"] = accept_header_value
    headers[user_agent_header] = self.sdk_configuration.user_agent
    ...
```

### Operation ID issues and solutions

The unique `operation_id` generated by FastAPI does not translate well into an SDK. We need to customize the unique `operation_id` that FastAPI generates for better readability.

For instance, in the operation that returns a burger by `burger_id`, the default unique ID would be `read_burger_burger__burger_id__get`. This makes its way into SDK code, leading to class names such as `ReadBurgerBurgerBurgerIDGetRequest` or function names like `read_burger_burger_burger_id_get`.

Here's a usage example after generating an SDK without customizing the `operationId`:

```python
import sdk
from sdk.models import operations

s = sdk.SDK()

req = operations.ReadBurgerBurgerBurgerIDGetRequest(
    burger_id=847252,
)

res = s.burger.read_burger_burger_burger_id_get(req)
```

However, after using the custom function `generate_unique_id` we defined previously, the `read_burger` operation gets a friendlier operation ID, `readBurger`, and the usage example becomes easier to read:

```python
import sdk
from sdk.models import operations

s = sdk.SDK()

req = operations.ReadBurgerRequest(
    burger_id=847252,
)

res = s.burger.read_burger(req)
```

In addition to the two methods described earlier, there is a third way to customize the `operation_id`. We can add the top-level `x-speakeasy-name-override` [Speakeasy extension](https://www.speakeasy.com/docs/speakeasy-reference/extensions) to our OpenAPI document, allowing Speakeasy to override the generated names when it generates SDK code.
To add this extension, follow the Speakeasy guide to [changing method names](/docs/customize-sdks/methods).

```yaml filename="name-override.yaml"
# Example of x-speakeasy-name-override extension
x-speakeasy-name-override:
  operations:
    readBurger: getBurger
    createBurger: addBurger
```

### Adding retry functionality

Speakeasy can generate SDKs that follow custom rules for retrying failed requests. For instance, if your server fails to return a response within a specified time, you may want your users to retry their request without clobbering your server.

To add retries to SDKs generated by Speakeasy, add a top-level `x-speakeasy-retries` schema to your OpenAPI document. You can also override the retry strategy per operation by adding `x-speakeasy-retries` to each operation:

```yaml filename="speakeasy-retries.yaml"
# Speakeasy retries can be configured with a top-level extension
x-speakeasy-retries:
  strategy: backoff
  statusCodes: [5XX]
  retryConnectionErrors: true
```

To add global retries, we need to customize the schema generated by the FastAPI `get_openapi` function:

```python
from fastapi import FastAPI
from fastapi.openapi.utils import get_openapi

app = FastAPI(
    summary="A simple API to manage burgers and orders",
    description="This API is used to manage burgers and orders in a restaurant",
    version="0.1.0",
    title="APItizing Burger API",
)

def custom_openapi():
    if app.openapi_schema:
        return app.openapi_schema

    openapi_schema = get_openapi(
        title=app.title,
        version=app.version,
        summary=app.summary,
        description=app.description,
        routes=app.routes,
    )

    # Add retries
    openapi_schema["x-speakeasy-retries"] = {
        "strategy": "backoff",
        "backoff": {
            "initialInterval": 500,
            "maxInterval": 60000,
            "maxElapsedTime": 3600000,
            "exponent": 1.5,
        },
        "statusCodes": [
            "5XX",
        ],
        "retryConnectionErrors": True,
    }

    app.openapi_schema = openapi_schema
    return app.openapi_schema

app.openapi = custom_openapi
```

Keep in mind that you'll need to add this customization _after_ declaring your operation routes.

This change adds the following top-level section to `openapi.yaml`:

```yaml
x-speakeasy-retries:
  backoff:
    exponent: 1.5
    initialInterval: 500
    maxElapsedTime: 3600000
    maxInterval: 60000
  retryConnectionErrors: true
  statusCodes:
    - 5XX
  strategy: backoff
```

To add `x-speakeasy-retries` to a single operation, update the operation and add the `openapi_extra` parameter as follows:

```python
from fastapi import FastAPI

app = FastAPI()

@app.get(
    "/burger/",
    openapi_extra={
        "x-speakeasy-retries": {
            "strategy": "backoff",
            "backoff": {
                "initialInterval": 500,
                "maxInterval": 60000,
                "maxElapsedTime": 3600000,
                "exponent": 1.5,
            },
            "statusCodes": [
                "5XX",
            ],
            "retryConnectionErrors": True,
        }
    },
)
def list_burgers():
    return []
```

### Authentication and security

FastAPI supports several authentication mechanisms that can easily be integrated into your API.

The example below demonstrates adding an API key authentication scheme to the `/burger/` endpoint of our API. We use the `APIKeyHeader` dependency to validate the API key passed in the `Authorization` header:

```python
from fastapi.security import APIKeyHeader

API_KEY = "your-apitizing-api-key"

header_scheme = APIKeyHeader(
    name=API_KEY,
    auto_error=True,
    description="API Key for the Burger listing API. API Key should be sent as a header, with the value 'your-apitizing-api-key'",
    scheme_name="api_key",
)
```

We can pass a `key` parameter to the `list_burgers` function, retrieve the API key from the header, and perform validation:

```python
@app.get(
    "/burger/",
    response_model=List[BurgerOutput],
    tags=["burger"],
    ...
)
def list_burgers(key: str = Depends(header_scheme)):
    """List all burgers"""

    if key != API_KEY:
        raise HTTPException(status_code=401, detail="Invalid API Key")

    return [BurgerOutput(**burger_data.dict()) for burger_data in burgers_db]
```

Now when generating the OpenAPI document, the API key authentication scheme will be included and only required for the listing on the `/burger/` endpoint.

### Handling form data

Form data is a common way to receive information from clients, particularly in web applications. FastAPI provides robust support for form data through its `Form` class, and it correctly documents these form fields in the OpenAPI schema.

When working with form data in FastAPI, you need to use the `Form` class from the `fastapi` module. This ensures that FastAPI correctly adds the appropriate schema information to your OpenAPI document.

```python
from fastapi import FastAPI, Form
from typing import Annotated

app = FastAPI()

@app.post("/burger/create/")
async def create_burger_form(
    name: Annotated[str, Form()],
    description: Annotated[str, Form()],
    price: Annotated[float, Form()]
):
    """Create a new burger using form data"""
    return {
        "name": name,
        "description": description,
        "price": price
    }
```

In the OpenAPI document, FastAPI correctly identifies this endpoint as accepting form data:

```yaml
paths:
  /burger/create/:
    post:
      summary: Create Burger Form
      operationId: createBurgerForm
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                name:
                  type: string
                description:
                  type: string
                price:
                  type: number
              required:
                - name
                - description
                - price
      responses:
        "200":
          description: Successful Response
          content:
            application/json:
              schema: {}
      tags:
        - burger
```

When Speakeasy generates an SDK from this OpenAPI document, it will correctly handle form data submissions. The generated SDK will provide a clean interface for submitting form data:

```python
import sdk
from sdk.models import operations

s = sdk.SDK()

req = operations.CreateBurgerFormRequest(
    name="Classic Burger",
    description="Our signature burger with special sauce",
    price=8.99
)

res = s.burger.create_burger_form(req)
```

#### File uploads

FastAPI also supports file uploads, both as individual files or as multiple files. The OpenAPI schema will correctly document these endpoints as accepting multipart form data.

```python
from fastapi import FastAPI, File, UploadFile
from typing import Annotated

app = FastAPI()

@app.post("/burger/image/")
async def upload_burger_image(
    burger_id: Annotated[int, Form()],
    image: Annotated[UploadFile, File()]
):
    """Upload an image for a burger"""
    contents = await image.read()
    # Process file contents...
    return {
        "burger_id": burger_id,
        "filename": image.filename,
        "content_type": image.content_type,
        "size": len(contents)
    }
```

This endpoint will be documented in the OpenAPI schema with `multipart/form-data` content type, allowing Speakeasy to generate appropriate SDK code for handling file uploads.

#### Advanced form validation

For form data that needs validation beyond simple type checking, you can combine Pydantic models with the `Form` class. First, define your model with the validation rules:

```python
from pydantic import BaseModel, Field

class BurgerFormData(BaseModel):
    name: str = Field(..., min_length=3, max_length=50)
    description: str = Field(..., min_length=10, max_length=200)
    price: float = Field(..., gt=0, le=100)
```

Then use the model fields with form data:

```python
@app.post("/burger/create/validated/")
async def create_burger_validated(
    name: Annotated[str, Form()],
    description: Annotated[str, Form()],
    price: Annotated[float, Form()]
):
    burger_data = BurgerFormData(
        name=name,
        description=description,
        price=price
    )
    return burger_data
```

FastAPI will generate an OpenAPI document that includes these validation constraints, allowing SDK users to understand the requirements before submitting the form.

## Summary

In this post, we've explored how you can set up a FastAPI-based SDK generation pipeline without hand-editing or updating OpenAPI documents. By using existing FastAPI methods for extending and customizing OpenAPI documents, you can improve the usability of your generated client SDKs.
