Home → Articles → How to Use Pydantic Models with FastAPI to Validate Request Data

How to Use Pydantic Models with FastAPI to Validate Request Data

Published: Last updated:

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 curl for 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-demo project directory

    console
    $ mkdir fastapi-pydantic-demo
    
  • Switch to the new fastapi-pydantic-demo directory.

    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/activate
    

    On 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.py file with Nano.

    console
    $ nano main.py
    
  • Add the following code to main.py.

    Python
    from 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.py for 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.py file now looks like this:

    Python
    from 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 price field.

    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.py for editing.

    console
    $ nano main.py
    
  • Define an output model that excludes sensitive or unnecessary fields.

    Python
    class ItemOut(BaseModel):
        name: str
        price: float
    
  • Add the response_model parameter 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.py file now looks like this:

    Python
    from 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_offer field 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_offer field 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.py for editing.

    console
    $ nano main.py
    
  • Import Field and EmailStr from Pydantic.

    Python
    from pydantic import BaseModel, Field, EmailStr
    
  • Define a model with constrained fields.

    Python
    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")
    
  • 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.py file now looks like this:

    Python
    from 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.py for editing.

    console
    $ nano main.py
    
  • Import List from the typing module.

    Python
    from typing import List
    
  • Define an inner model for nested items.

    Python
    class Item(BaseModel):
        name: str
        price: float
    
  • Define an outer model that contains a list of the inner model.

    Python
    class 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.py file now looks like this:

    Python
    from 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 price field.

    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.py for editing.

    console
    $ nano main.py
    
  • Import model_validator and date from their respective modules.

    Python
    from pydantic import BaseModel, model_validator
    from datetime import date
    
  • Define a model with a custom validation method.

    Python
    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
    
  • 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.py file now looks like this:

    Python
    from 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.py file.

    console
    $ nano test_main.py
    
  • Add the following code to test_main.py.

    Python
    from 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 -v
    

    Sample 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 -v flag 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.

console
$ 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.