Skip to content

5.9 Forms

Yes, you can use Form() instead of Field() if you want FastAPI to expect the values as form-data instead of JSON in the request body.

The difference is:

  • Field(...) → For JSON body parameters in Pydantic models.
  • Form(...) → For form submissions (application/x-www-form-urlencoded or multipart/form-data).

Here’s how it would look with Form and descriptions:

from fastapi import Form
from pydantic import BaseModel, EmailStr
class UserBase(BaseModel):
email: EmailStr
full_name: str
@classmethod
def as_form(
cls,
email: EmailStr = Form(..., description="The user's email address (must be valid).", examples=["alice@example.com", "chathurdara@gmail.com"]),
full_name: str = Form(..., description="The user's full name.", examples=["Alice Wonderland", "Nadith Pathirage"])
):
return cls(email=email, full_name=full_name)

Then in your endpoint:

from fastapi import FastAPI, Depends
app = FastAPI()
@app.post("/users")
def create_user(user: UserBase = Depends(UserBase.as_form)):
return user

Key points:

  • You can pass description and examples to Form() just like Field().
  • Form() is not a Pydantic feature — it’s a FastAPI dependency injection feature.
  • If you switch to Form(), Swagger will show the parameters as form-data instead of a JSON body.

If you want, I can show you a cleaner way without the as_form boilerplate, so you don’t have to manually define that method. That’s a common trick in FastAPI form handling.

=========================

Alright — the cleaner way is to skip manually writing an as_form method and instead use Form() directly in your endpoint function parameters.

Example:

from fastapi import FastAPI, Form
from pydantic import EmailStr
app = FastAPI()
@app.post("/users")
def create_user(
email: EmailStr = Form(..., description="The user's email address (must be valid).", examples=["alice@example.com", "chathurdara@gmail.com"]),
full_name: str = Form(..., description="The user's full name.", examples=["Alice Wonderland", "Nadith Pathirage"])
):
return {"email": email, "full_name": full_name}

What happens here:

  • Form(...) tells FastAPI to read that value from form-data.
  • description and examples still work in Swagger docs.
  • You avoid creating a separate Pydantic model just for forms.

💡 When to still use the as_form trick

  • If you want the same Pydantic model to work for both JSON and form-data without rewriting all fields in the route.
  • Keeps validation in one place.

If you want, I can show you a decorator that automatically adds .as_form() to all your Pydantic models so you never write boilerplate again. That’s what I usually use for larger APIs.

=========================

Gotcha — here’s the decorator version so your Pydantic model can be used with both JSON and form-data without writing the as_form method every time.

from fastapi import Form
from pydantic import BaseModel
from typing import Type
def as_form(cls: Type[BaseModel]):
"""
Adds an .as_form class method to Pydantic models so they can be used with Form().
"""
new_params = []
for field_name, model_field in cls.__fields__.items():
default = Form(
... if model_field.required else model_field.default,
description=model_field.field_info.description,
examples=model_field.field_info.extra.get("examples")
)
new_params.append((field_name, default))
# Create the method dynamically
def _as_form(**data):
return cls(**data)
# Add default values to the signature
from inspect import signature, Parameter
sig = signature(_as_form)
params = [
Parameter(
name=name,
kind=Parameter.POSITIONAL_OR_KEYWORD,
default=default
)
for name, default in new_params
]
_as_form.__signature__ = sig.replace(parameters=params) # type: ignore
setattr(cls, "as_form", classmethod(_as_form))
return cls
from pydantic import BaseModel, EmailStr, Field
from fastapi import FastAPI, Depends
app = FastAPI()
@as_form
class UserBase(BaseModel):
email: EmailStr = Field(
...,
description="The user's email address (must be valid).",
examples=["alice@example.com", "chathurdara@gmail.com"]
)
full_name: str = Field(
...,
description="The user's full name.",
examples=["Alice Wonderland", "Nadith Pathirage"]
)
@app.post("/users")
def create_user(user: UserBase = Depends(UserBase.as_form)):
return user
  • You still write your model once using Field() for descriptions & examples.
  • @as_form automatically adds .as_form() so FastAPI can use it with Form().
  • Swagger will show form-data fields with your descriptions & examples.

If you want, I can trim this decorator down so it’s just 10 lines instead of this verbose version — easier to maintain in your codebase. Would you like me to do that?

=============================

The whole reason people use an as_form method in FastAPI is because of how FastAPI treats request bodies.

Here’s the problem in short:

  • Pydantic models (BaseModel) → By default, FastAPI expects them as JSON in the body.
  • Form data (Form()) → FastAPI expects each field to be declared as a function parameter with Form(...).

If you try to just replace Field() with Form() inside your Pydantic model, it won’t work — Form() is not part of Pydantic, it’s a FastAPI dependency marker.


as_form is simply a helper to make your model usable for form-data without rewriting all fields in your route function. It does two things:

  1. Defines a class method (e.g., UserBase.as_form) that declares each model field as a FastAPI Form(...) parameter.
  2. Lets you keep all your validation, descriptions, examples inside the Pydantic model instead of duplicating them in the route.

@app.post("/users")
def create_user(
email: EmailStr = Form(..., description="The user's email address"),
full_name: str = Form(..., description="The user's full name")
):
return {"email": email, "full_name": full_name}

❌ You have to repeat every field & validation here.


@app.post("/users")
def create_user(user: UserBase = Depends(UserBase.as_form)):
return user

✅ You define fields once in the model and reuse them for form-data.


Basically:

  • as_form = “Make this model work with Form() without rewriting everything.”
  • It’s just syntactic sugar — FastAPI doesn’t require it, but it saves a lot of code in bigger APIs.

If you want, I can show you exactly how FastAPI internally reads form-data with models, so as_form will make even more sense. That would explain why you can’t just Field(..., Form) inside a model.

==========================================

Right — that’s because once you switch to Form(...) (or File(...)) in FastAPI, you’re no longer dealing with a JSON request body, so Swagger can’t render the nice JSON schema like it does for application/json.

FastAPI + OpenAPI work like this:

  • Model in JSON body → OpenAPI shows a full JSON schema in Swagger.
  • Form parameters → OpenAPI treats each field as an individual parameter in application/x-www-form-urlencoded or multipart/form-data. Swagger shows them as separate inputs, not as one JSON object.

That’s why your JobBase.as_form example only shows a list of fields in Swagger, not a nested JSON example.


OpenAPI does not support showing a nested JSON schema for application/x-www-form-urlencoded bodies the same way it does for application/json. It sees them as flat parameters.

So in Swagger:

  • Body(..., example=...) → JSON schema with example.
  • Form(...) → Table of form fields with types & descriptions.

If you really want Swagger to still show the JSON schema but also accept form-data, you have three main options:

  1. Accept JSON in Swagger, but parse form-data in code (Not great if you actually expect form uploads.)
  2. Use Body() for JSON schema and separate file uploads with Form()/File().
  3. Write a custom OpenAPI schema override to fake the nested form-data object in Swagger (works visually but is misleading because the request type is still form-data).

If you tell me which one you want —

  • keep JSON schema look in Swagger,
  • or keep real form-data but just improve how Swagger displays it — I can show you the exact code to do that.

Which way do you want to go?

https://fastapi.tiangolo.com/tutorial/schema-extra-example/#openapi-examples-in-the-docs-ui