In the last post, Body - Multiple Parameters, we learned how to attach validation to whole body parameters with Body(), like Body(gt=0) on a singular value. That works great at the parameter level. But what about the fields inside a Pydantic model? š¤
Our Blog model has a title and content, but right now any string sails through. An empty title? Fine. A 5000-character title? Also fine. We want rules on the individual fields, and we want them living right inside the model where they belong. That is exactly the job of Pydantic's Field.
Importing Field
Here is the one thing that trips people up, so let's get it out of the way first.
from pydantic import BaseModel, FieldLet's review:
Fieldcomes frompydantic, not fromfastapi. Notice the difference fromQuery,Path, andBody, which all come fromfastapi.It sits next to
BaseModelin the same import line, since both are Pydantic tools.
Mix this up and you will get an import error wondering where Field went. Keep it with Pydantic. š
Adding rules to model fields
Now let's put Field to work on our Blog model.
from typing import Annotated, Optionalfrom fastapi import Body, FastAPIfrom pydantic import BaseModel, Fieldapp = FastAPI()class Blog(BaseModel): title: str = Field(min_length=3, max_length=100) content: str = Field(description="The full body of the blog post") rating: Optional[float] = Field(default=None, gt=0, le=5) is_active: bool = True@app.put("/blog/{blog_id}")async def update_blog(blog_id: int, blog: Annotated[Blog, Body(embed=True)]): return {"blog_id": blog_id, "blog": blog}Let's review:
titlenow must be between 3 and 100 characters. Send a one-letter title and FastAPI rejects it before your code runs.contentcarries adescription. That does not validate anything, it just enriches the auto-generated docs so the field explains itself.ratingis optional with a default ofNone, but if it is sent it must be greater than 0 and no more than 5. That is the samegtandlewe used withPathnumbers earlier.is_activestays a plain field with a default. Not every field needs aField. Use it only where you want extra rules or metadata.
If a client sends a rating of 9, FastAPI sends back a clean validation error pointing right at the rating field. No manual checks anywhere in your function.
Field looks just like Query, Path, and Body
Here is the nice part. Field takes the same arguments you already know: gt, ge, lt, le, min_length, max_length, title, description, and so on. If you learned them for Query and Path, you already know Field.
class Blog(BaseModel): title: str = Field(min_length=3, max_length=100, title="Blog title") content: str = Field(min_length=10) rating: Optional[float] = Field(default=None, ge=0, le=5)Let's review:
Same keyword arguments, just declared inside the model instead of in the function signature.
The structure mirrors a function parameter: a type, a default, and a
Fieldfor the extras. SwapPathorQueryforFieldand it reads the same way.
One thing to watch with Annotated: for a field default, you can keep it simple and pass default=None straight to Field, as we did above. It keeps the optional field tidy in one place.
Field is imported from pydantic, while Query, Path, and Body come from fastapi. They behave almost identically, but they live in different packages. When an import error shows up here, this swap is usually the reason. š
āļø Up next: Body - Nested Models. So far our Blog has been flat, just strings, numbers, and a bool. But what if a blog needs a list of tags, or an Author model nested right inside it? Pydantic handles models inside models with the same validation magic, and that is where we head next.
