FastAPI

Data Validation, Pydantic schemas in FastAPI

Here we will discuss on basic concepts which are essentials for the fastapi developer to understand the workflow of fastapi in real world applications.

Path and Query Parameters 

Fastapi offers two ways to capture data from URLs in your application: path parameters and query parameters.  They both server different purposes and have distinct way of being decleared in code. 

Path Parameters: 

  • typically used to identify  resources (eg: userid)
  • it is designed to capture specific segment with in URL path.
  • They act like variable that are filled in based on the corresponding part of URL.They  are define using curly braces {} in route path.
  •  eg: /users/{user_id}
  • Declaring in FastAPI: use Path function from fastapi when defining your function arguments, you can aslo sepcify datatypes and descriptions.

Query Parameters: 

  • Used For Filtering,sorting and pagination
  • it is used to capture optional key-value pair appended to the URL after question mark (?). they provide additional filtering or data specific to the request.
  •  You might have a api endpoint to get a list of user and  you want to allow filtering by name, you could use query parameters like /users?name=amrit
  • Declaring in fastapi while not mandatory for query parameters, it's good pratice to use the query function from fastapi.

Example:

from fastapi import FastAPI,status
from fastapi.responses import JSONResponse
from database import list_of_student

app = FastAPI()

def all_users():
    return list_of_student

def get_user_by_id(id,users):
    for user in users:
        if id==user.get('id'):
            return user
    return False  

@app.get("/users")
async def get_all_user(name:str=None):
    users=all_users()
    if name:
        users=[user for user in  users if name in user['name']]
    return JSONResponse(content={"data":users,"message":"Users List"},status_code=status.HTTP_200_OK)

@app.get("/user/{user_id}")
async def fetch_user(user_id:int,name:str=None):
    result=get_user_by_id(user_id,all_users())
    if result:
        return JSONResponse(content={"data":result,"message":"User fetch Successfully"},status_code=status.HTTP_200_OK)
    return JSONResponse(content={"data":result,"message":"User not found"},status_code=status.HTTP_404_NOT_FOUND)

Accessing Form Data 

# pip install python-multipart
from fastapi import Form
@app.post("/submit-form")
async def submit_form(username:str=Form(...),password:str=Form()):
    return {"username":username,"password":password}

 

working with file

from fastapi import FastAPI,File,UploadFile
from fastapi.responses import JSONResponse
import os
app=FastAPI()
UPLOAD_FOLDER='uploads'
os.makedirs(UPLOAD_FOLDER,exist_ok=True)

@app.post('/upload_file')
async def upload_file(file:UploadFile=File(...)):
    #access uploaded file using UploadFile parameter
    file_path=os.path.join(UPLOAD_FOLDER,file.filename)
    with open (file_path,"wb") as buffer:
        buffer.write(await file.read())
    return JSONResponse(content={"message":"file uploaded successfully","filename":file.filename,"content_type":file.content_type})

for multiple file

@app.post('/upload_files')
async def upload_files(files: list[UploadFile] = File(...)):
    uploaded_files_info = []

    for file in files:
        file_path = os.path.join(UPLOAD_FOLDER, file.filename)
        with open(file_path, "wb") as buffer:
            buffer.write(await file.read())
        
        uploaded_files_info.append({
            "filename": file.filename,
            "content_type": file.content_type,
            "file_path": file_path
        })

    return JSONResponse(
        content={
            "message": "Files uploaded successfully",
            "uploaded_files": uploaded_files_info
        }
    )

Validation 

query parameter validation

you can define validation directly ion the route handlers function using fast api's query parameter decorator.

from fastapi import Query
@app.get("/student")
def student(q:str=Query(None,min_length=1,max_length=5)):
    return {"q":q}

path parameter validation

validation route can be applied directly with in the route path declaration.

from fastapi import Path
@app.get("/student/{student_id}")
def student(student_id:int=Path(...,title="Id Of the Student",gt=0)):
    return {"Student_id":student_id}

Body Parameter Validation

For request Bodies , pydantic models are commonly used to define structure and validation rules.Validation is automatically done by pydantic

you can learn more about it from https://docs.pydantic.dev/latest/

pydantic is the data validation library in python, which i s widely used in fastapi for validating request and response data.

pydantic provides a way to define schemas using python classes, allowing you to specify the structure,type and validation rules for your data.

 

from pydantic import BaseModel,validator,Field, model_validator, ValidationError
from typing import Optional,Union,List

class Student(BaseModel):
    name: str
    age:int
    address:str
    country: Optional[str] = "Nepal" 
    no_of_subjects: int = Field(..., gt=5, description="Number of subjects")
    hobbies_and_interests: Union[List[str], str] = Field(default_factory=list)

    @validator("age")
    def age_validation(cls, v):
        if v<=0 or v>100:
            raise ValueError("Age must be in between 1 to 100")
        return v
        
   @validator("name", pre=True, always=True)
    def handle_empty_string(cls, value):
        if value == "":  # If the value is an empty string
            return None
        return value
        
    @model_validator(mode="before")
    @classmethod
    def check_for_username_email(cls, values):
        email = values.get("email")
        username = values.get("username")

        # Ensure email and username are not the same
        if email and username and email.split('@')[0] == username:
            raise ValueError("Email username part should not be the same as the username.")
        
        return values  # Must return modified values dictionary

 

More Info Related To Pydantic

from pydantic import BaseModel,Field,EmailStr
from datetime import datetime,timezone
class User(BaseModel):
    name: str = Field(..., alias="username")
    email:EmailStr
    age:int
    created_time: datetime | None = datetime.now(timezone.utc)
    read_time: datetime | None = None

    class Config:
        '''
        This allows you to create (or populate) the model using the field names, 
        even if you’ve set alias for the fields.
        '''
        allow_population_by_field_name = True
        '''
        This tells Pydantic how to serialize (.json(), .dict(), etc.) 
        specific types—in this case, datetime—into a JSON-friendly format
        '''
        # isoformt() will convert the datetime object into the string
        json_encoders = {datetime: lambda v: v.isoformat()}

allow_population_by_field_name = True

By default, Pydantic expects the input dictionary to use the alias "username". But with allow_population_by_field_name = True, you can also use the original Python name  name when creating the model:

User(name="panta", ...)  # This works if allow_population_by_field_name = True

Without this setting, you’d be forced to use only "username" when creating the object.

json_encoders = {datetime: lambda v: v.isoformat()}

This tells Pydantic how to serialize (.json(), .dict(), etc.) specific types—in this case, datetime—into a JSON-friendly format.

In programming, serialization means converting a complex data object (like a Python object) into a format that can be easily stored or sent (e.g., as JSON, XML, or a string).

In the context of Pydantic:

When we say Pydantic serializes a model:

  • It converts the model (a Python object) into a dictionary (.dict()) or a JSON string (.json()).
  • During this process, non-primitive types like datetime, Decimal, etc., need special handling to become string-compatible.

Example

from pydantic import BaseModel
from datetime import datetime, timezone

class Example(BaseModel):
    timestamp: datetime

    class Config:
        json_encoders = {datetime: lambda v: v.isoformat()}

data = Example(timestamp=datetime.now(timezone.utc))
print(data.json())

Output:

{"timestamp": "2025-05-05T14:30:00+00:00"}
  • datetime.now(timezone.utc) is a Python object.
  • .json() serializes it into a string that is JSON-compatible using .isoformat().

 

Specifically, json_encoders = {datetime: lambda v: v.isoformat()} it's used when you call:

  • .json() — converts the model to a JSON string.
  • .dict() (with by_alias=True or other options).
  • .model_dump() or .model_dump_json() in Pydantic v2.
  • When using the model as a response in a FastAPI endpoint (because FastAPI internally calls .json() or similar).

 

It is not used when:

  • You're just creating the model.
  • You're saving data to a database (e.g., in SQLAlchemy).
  • You're reading data and passing it into the model

 

Why it's needed:

Python datetime objects are not JSON serializable by default. JSON supports strings, so we need to convert datetime to a string.

How it works:

This encoder uses datetime.isoformat() to convert a datetime object into an ISO 8601 formatted string (e.g., "2024-05-05T10:30:00+00:00").

Example:

from datetime import datetime, timezone

n = Notification(
    customer_member_id="123",
    notification_type=NotificationType.info,
    message="New study available"
)

print(n.json())

With the encoder, the created_time field (which is a datetime) is automatically converted to an ISO string in the output JSON.


About author

author image

Amrit panta

Fullstack developer, content creator



Scroll to Top