How to Use Pydantic Models with FastAPI to Validate Request Data
02 Apr, 2026
Introduction
Pydantic is a data validation library for Python that uses type annotations to define data shapes and enforce rules. When you build APIs with FastAPI, Pydantic models serve as the contract between your application and its clients, ensuring incoming data meets your requirements before your code processes it. This validation layer prevents malformed or incorrect data from reaching your business logic, reducing bugs and improving security. FastAPI integrates Pydantic directly into its request handling pipeline, so you define your data models once and get validation, serialization, and automatic documentation without extra work.
This guide teaches you how to create Pydantic models and use them in FastAPI endpoints for request validation.
Prerequisites
Before you start:
- Set up a Python environment with Python 3.8 or newer
- Prepare an API client like
curlfor testing your endpoints - Understand Python type hints, as Pydantic relies on them for validation
Set Up a Python Virtual Environment
A virtual environment keeps your project dependencies separate from other Python projects on your system. This practice prevents version conflicts and makes your project portable. Create a dedicated directory for your project and set up a virtual environment inside it.
-
Create a new
fastapi-pydantic-demoproject directoryconsole$ mkdir fastapi-pydantic-demo -
Switch to the new
fastapi-pydantic-demodirectory.console$ cd fastapi-pydantic-demo -
Create the virtual environment named
venv.console$ python3 -m venv venv -
Activate the virtual environment.
On Linux or Mac OS:
console$ source venv/bin/activateOn Windows:
console$ venv\Scripts\activate -
Install FastAPI, a modern web framework for building APIs.
console$ pip install fastapi -
Install Uvicorn, an ASGI server that runs your FastAPI application.
console$ pip install uvicorn -
Install Pydantic, the data validation library FastAPI uses internally.
console$ pip install pydantic
Create a Basic Pydantic Model
A Pydantic model is a Python class that inherits from BaseModel. You define fields using type annotations, and Pydantic automatically checks that incoming data matches those types. To define your first model, create the new main.py file with the Nano text editor.
-
Create the new
main.pyfile with Nano.console$ nano main.py -
Add the following code to
main.py.Pythonfrom fastapi import FastAPI from pydantic import BaseModel app = FastAPI() class Item(BaseModel): name: str price: float is_offer: bool | None = None -
Save and close the file by pressing Ctrl + X + Y.
In this model, name and price are required fields. FastAPI returns a validation error if a request omits them. The is_offer field has a default value of None, making it optional. The | None syntax tells Pydantic this field can accept either a boolean value or None.
Use a Pydantic Model in a POST Endpoint
To validate incoming request data, add a Pydantic model as a parameter to your endpoint function. FastAPI reads the request body, parses it as JSON, and validates it against your model. When validation passes, your function receives an instance of the model with the parsed data. Open the main.py file again to add the endpoint.
-
Open
main.pyfor editing.console$ nano main.py -
Add a POST endpoint that accepts the Pydantic model.
Python@app.post("/items/") async def create_item(item: Item): return {"item_name": item.name, "item_price": item.price} -
Save and close the file by pressing Ctrl + X + Y.
The complete
main.pyfile now looks like this:Pythonfrom fastapi import FastAPI from pydantic import BaseModel app = FastAPI() class Item(BaseModel): name: str price: float is_offer: bool | None = None @app.post("/items/") async def create_item(item: Item): return {"item_name": item.name, "item_price": item.price}
When a client sends a POST request to /items/ with a JSON body, FastAPI checks that name exists and is a string, and that price exists and is a number. If validation fails, FastAPI returns a 422 Unprocessable Entity response with details about what went wrong.
-
Test a successful request, run the following command.
console$ curl -X POST http://localhost:8000/items/ -H "Content-Type: application/json" -d '{"name":"Sample Item","price":19.99}'Sample output:
```json {"item_name":"Sample Item","item_price":19.99} ``` -
Test a failed request that omits the required
pricefield.console$ curl -X POST http://localhost:8000/items/ -H "Content-Type: application/json" -d '{"name":"Sample Item"}'Sample output:
JSON{"detail":[{"type":"missing","loc":["body","price"],"msg":"Field required","input":{"name":"Sample Item"}}]}
Validate Responses with response_model
You can also validate outgoing responses by adding the response_model parameter to your route decorator. This ensures your API returns data in the promised shape and filters out any extra fields you might accidentally include. The response_model parameter accepts a Pydantic model class. Open the main.py file to add an output model.
-
Open
main.pyfor editing.console$ nano main.py -
Define an output model that excludes sensitive or unnecessary fields.
Pythonclass ItemOut(BaseModel): name: str price: float -
Add the
response_modelparameter to your endpoint decorator.Python@app.post("/items/", response_model=ItemOut, status_code=201) async def create_item(item: Item): return item -
Save and close the file by pressing Ctrl + X + Y.
The complete
main.pyfile now looks like this:Pythonfrom fastapi import FastAPI from pydantic import BaseModel app = FastAPI() class Item(BaseModel): name: str price: float is_offer: bool | None = None class ItemOut(BaseModel): name: str price: float @app.post("/items/", response_model=ItemOut, status_code=201) async def create_item(item: Item): return item
Using response_model serves two purposes. First, it documents your API's output structure in the automatically generated OpenAPI documentation. Second, it filters response data, preventing extra fields from leaking out. If you return a Response object directly from your endpoint, FastAPI skips response_model validation, so always return the model instance itself.
-
Test the response filtering by sending a request and observe that the
is_offerfield does not appear in the response.console$ curl -X POST http://localhost:8000/items/ -H "Content-Type: application/json" -d '{"name":"Filtered Item","price":29.99,"is_offer":true}'Sample output:
JSON{"name":"Filtered Item","price":29.99} -
Notice that the response excludes the
is_offerfield even though the request included it.
Add Field Constraints
Pydantic provides the Field function to add validation rules beyond basic type checking. You can enforce minimum and maximum lengths for strings, set numeric ranges, and provide descriptions for documentation. Import Field from pydantic and use it in your model definitions. Open the main.py file to add constrained fields.
-
Install
email-validator, a library that checks email address formats.console$ pip install email-validator -
Open
main.pyfor editing.console$ nano main.py -
Import
FieldandEmailStrfrom Pydantic.Pythonfrom pydantic import BaseModel, Field, EmailStr -
Define a model with constrained fields.
Pythonclass User(BaseModel): name: str = Field(..., min_length=3, max_length=50, description="Name must be between 3 and 50 characters") age: int = Field(..., gt=0, le=120, description="Age must be between 1 and 120") email: EmailStr = Field(..., description="Must be a valid email address") -
Add an endpoint to test the constrained model.
Python@app.post("/users/") async def create_user(user: User): return {"message": f"User {user.name} created successfully"} -
Save and close the file by pressing Ctrl + X + Y.
The complete
main.pyfile now looks like this:Pythonfrom fastapi import FastAPI from pydantic import BaseModel, Field, EmailStr app = FastAPI() class User(BaseModel): name: str = Field(..., min_length=3, max_length=50, description="Name must be between 3 and 50 characters") age: int = Field(..., gt=0, le=120, description="Age must be between 1 and 120") email: EmailStr = Field(..., description="Must be a valid email address") @app.post("/users/") async def create_user(user: User): return {"message": f"User {user.name} created successfully"}The
Field(..., min_length=3)syntax uses the ellipsis (...) to mark the field as required. Pydantic automatically checks that the email string follows a valid format.-
Test the field constraints by sending a request with a name that is too short.
console$ curl -X POST http://localhost:8000/users/ -H "Content-Type: application/json" -d '{"name":"Jo","age":25,"email":"john@example.com"}'Sample output:
JSON{"detail":[{"type":"string_too_short","loc":["body","name"],"msg":"String should have at least 3 characters","input":"Jo","ctx":{"min_length":3}}]} -
Send a valid request.
console$ curl -X POST http://localhost:8000/users/ -H "Content-Type: application/json" -d '{"name":"John Doe","age":30,"email":"john.doe@example.com"}'Sample output:
JSON{"message":"User John Doe created successfully"}
-
Work with Nested Models
Real-world APIs often handle complex, nested data structures. Pydantic lets you define models within models to represent these hierarchies. For a list of nested items, import List from typing and use it as a type annotation. Open the main.py file to add nested models.
-
Open
main.pyfor editing.console$ nano main.py -
Import
Listfrom the typing module.Pythonfrom typing import List -
Define an inner model for nested items.
Pythonclass Item(BaseModel): name: str price: float -
Define an outer model that contains a list of the inner model.
Pythonclass User(BaseModel): username: str items: List[Item] = [] -
Create an endpoint that accepts the nested model.
Python@app.post("/users/") async def create_user(user: User): return {"username": user.username, "item_count": len(user.items)} -
Save and close the file by pressing Ctrl + X + Y.
The complete
main.pyfile now looks like this:Pythonfrom fastapi import FastAPI from pydantic import BaseModel from typing import List app = FastAPI() class Item(BaseModel): name: str price: float class User(BaseModel): username: str items: List[Item] = [] @app.post("/users/") async def create_user(user: User): return {"username": user.username, "item_count": len(user.items)}
In this example, the User model contains a list of Item models. When a client sends JSON with nested items, FastAPI validates each item in the list against the Item model. If any item lacks a required field or has the wrong type, FastAPI returns a validation error.
-
Test the nested model by sending a request with a user and their items.
console$ curl -X POST http://localhost:8000/users/ -H "Content-Type: application/json" -d '{"username":"alice","items":[{"name":"laptop","price":999.99},{"name":"mouse","price":25.50}]}'Sample output:
JSON{"username":"alice","item_count":2} -
Send a request with an invalid item that lacks the required
pricefield.console$ curl -X POST http://localhost:8000/users/ -H "Content-Type: application/json" -d '{"username":"bob","items":[{"name":"book"}]}'Sample output:
```json {"detail":[{"type":"missing","loc":["body","items",0,"price"],"msg":"Field required","input":{"name":"book"}}]} ```
Create Custom Validators
For validation rules that involve multiple fields, use Pydantic's @model_validator decorator. This decorator runs after individual field validation and lets you check relationships between fields. In Pydantic V2, the @model_validator decorator requires mode='after' to run after all field validations complete. Open the main.py file to add a custom validator.
-
Open
main.pyfor editing.console$ nano main.py -
Import
model_validatoranddatefrom their respective modules.Pythonfrom pydantic import BaseModel, model_validator from datetime import date -
Define a model with a custom validation method.
Pythonclass Event(BaseModel): name: str start_date: date end_date: date @model_validator(mode='after') def check_dates(self): if self.start_date >= self.end_date: raise ValueError('start_date must be before end_date') return self -
Create an endpoint to test the custom validator.
Python@app.post("/events/") async def create_event(event: Event): return {"message": f"Event '{event.name}' created"} -
Save and close the file by pressing Ctrl + X + Y.
The complete
main.pyfile now looks like this:Pythonfrom fastapi import FastAPI from pydantic import BaseModel, model_validator from datetime import date app = FastAPI() class Event(BaseModel): name: str start_date: date end_date: date @model_validator(mode='after') def check_dates(self): if self.start_date >= self.end_date: raise ValueError('start_date must be before end_date') return self @app.post("/events/") async def create_event(event: Event): return {"message": f"Event '{event.name}' created"}
The @model_validator(mode='after') decorator tells Pydantic to run this method after all field validations succeed. If the validation fails, Pydantic raises a ValueError, and FastAPI converts it to a 422 Unprocessable Entity response.
-
Test the custom validator by sending a request with valid dates.
console$ curl -X POST http://localhost:8000/events/ -H "Content-Type: application/json" -d '{"name":"Conference","start_date":"2025-06-01","end_date":"2025-06-05"}'Sample output:
JSON{"message":"Event 'Conference' created"} -
Send a request where the end date comes before the start date.
console$ curl -X POST http://localhost:8000/events/ -H "Content-Type: application/json" -d '{"name":"Invalid Event","start_date":"2025-06-10","end_date":"2025-06-05"}'Sample output:
JSON{"detail":[{"type":"value_error","loc":["body"],"msg":"Value error, start_date must be before end_date","input":{"name":"Invalid Event","start_date":"2025-06-10","end_date":"2025-06-05"},"ctx":{"error":{}}}]}
Test Your API
Create a test file named test_main.py to verify your API works correctly. FastAPI provides a TestClient that simulates HTTP requests without running a server. Create the new test file with the Nano text editor.
-
Install pytest, a testing framework that discovers and runs your tests.
console$ pip install pytest -
Create the new
test_main.pyfile.console$ nano test_main.py -
Add the following code to
test_main.py.Pythonfrom fastapi.testclient import TestClient from main import app client = TestClient(app) def test_create_item(): response = client.post( "/items/", json={"name": "Test Item", "price": 10.99} ) assert response.status_code == 200 data = response.json() assert data["item_name"] == "Test Item" assert data["item_price"] == 10.99 def test_invalid_item(): response = client.post( "/items/", json={"name": "Test Item"} ) assert response.status_code == 422 -
Save and close the file by pressing Ctrl + X + Y.
-
Run your tests with the following command.
console$ pytest test_main.py -vSample output:
============================= test session starts ============================== collected 2 items test_main.py::test_create_item PASSED test_main.py::test_invalid_item PASSED ============================== 2 passed in 0.15s ===============================The
-vflag shows verbose output, listing each test and its result.
Run the FastAPI Application
Start your FastAPI application using Uvicorn, the ASGI server you installed earlier. The --reload flag enables automatic restarts when you change your code, which speeds up development.
$ uvicorn main:app --reload
Sample output:
INFO: Will watch for changes in these directories: ['/home/user/fastapi-pydantic-demo']
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO: Started reloader process [12345] using StatReload
INFO: Started server process [12346]
INFO: Waiting for application startup.
INFO: Application startup complete.
After the server starts, open your web browser to http://localhost:8000/docs. FastAPI automatically generates interactive API documentation from your Pydantic models, showing the expected request and response shapes for every endpoint.
Conclusion
In this guide, you learned how to use Pydantic models with FastAPI to validate request and response data. You created a virtual environment, built models with field constraints using Field, validated responses with response_model, handled nested data structures, and created custom validators for multi-field rules. Now that you have built a validated API, consider integrating a database like PostgreSQL using SQLAlchemy or explore Pydantic's advanced features such as recursive models and custom serialization for more complex scenarios.