How to access Request object & dependencies of FastAPI in models created from Pydantic's BaseModel

Question:

I am writing APIs using stack FastAPI, Pydantic & SQL Alchemy and I have come across many cases where I had to query database to perform validations on payload values. Let’s consider one example API, /forgot-password. This API will accept email in the payload and I need to validate the existence of the email in database. If the email exist in the database then necessary action like creating token and sending mail would be performed or else an error response against that field should be raise by Pydantic. The error responses must be the standard PydanticValueError response. This is because all the validation errors would have consistent responses as it becomes easy to handle for the consumers.

Payload –

{
    "email": "[email protected]"
}

In Pydantic this schema and the validation for email is implemented as –

class ForgotPasswordRequestSchema(BaseModel):
    email: EmailStr
    
    @validator("email")
    def validate_email(cls, v):
        # this is the db query I want to perform but 
        # I do not have access to the active session of this request.
        user = session.get(Users, email=v) 
        if not user:
            raise ValueError("Email does not exist in the database.")

        return v

Now this can be easily handled if the we simple create an Alchemy session in the pydantic model like this.

class ForgotPasswordRequestSchema(BaseModel):
    email: EmailStr
    _session = get_db() # this will simply return the session of database.
    _user = None
    
    @validator("email")
    def validate_email(cls, v):
        # Here I want to query on Users's model to see if the email exist in the 
        # database. If the email does. not exist then I would like to raise a custom 
        # python exception as shown below.

        user = cls._session.get(Users, email=v) # Here I can use session as I have 
        # already initialised it as a class variable.

        if not user:
            cls.session.close()
            raise ValueError("Email does not exist in the database.")

        cls._user = user # this is because we want to use user object in the request 
        # function.

        cls.session.close()

        return v

But it is not a right approach as through out the request only one session should be used. As you can see in above example we are closing the session so we won’t be able to use the user object in request function as user = payload._user. This means we will have to again query for the same row in request function. If we do not close the session then we are seeing alchemy exceptions like this – sqlalchemy.exc.PendingRollbackError.

Now, the best approach is to be able to use the same session in the Pydantic model which is created at the start of request and is also closing at the end of the request.

So, I am basically looking for a way to pass that session to Pydantic as context. Session to my request function is provided as dependency.

Asked By: Jeet Patel

||

Answers:

Don’t do that!

The purpose of pydantic classes is to store dictionaries in a legit way, as they have IDE support and are less error prone. The validators are there for very simple stuff that doesn’t touch other parts of system (like is integer positive or does email satisfy the regex).

Saying that, you should use the dependencies. That way you can be sure you have single session during processing all request and because of context manager the session will be closed in any case.

Final solution could look like this:

from fastapi import Body, Depends
from fastapi.exceptions import HTTPException

def get_db():
    db = your_session_maker
    try:
        yield db
    finally:
        db.close()

@app.post("/forgot-password/")
def forgot_password(email: str = Body(...), db: Session = Depends(get_db)):
    user = db.get(Users, email=email)
    if not user:
        # If you really need to, you can for some reason raise pydantic exception here
        raise HTTPException(status_code=400, detail="No email")
 
Answered By: kosciej16

It is not recommended to query the database in pydantic schema. Instead use session as a dependency.

If you want to raise errors like pydantic validation error you might need this:

def raise_custom_error(exc: Exception, loc: str, model: BaseModel, status_code=int, **kwargs):
    """
    This method will return error responses using pydantic error wrapper (similar to pydantic validation error).
    """
    raise HTTPException(
        detail=json.loads(ValidationError([ErrorWrapper(exc(**kwargs), loc=loc)], model=model).json()),
        status_code=status_code,
    )

Usage

class PayloadSchema(BaseModel):
    email: EmailStr

@app_router.post('/forgot-password')
def forgot_password(
    payload: PayloadSchema,
    session: Session = Depends(get_db),
    background_tasks: BackgroundTasks
):
    
    existing_user = db.get(Users, email=payload.email)
    if(existing_user):
        raise_custom_error(
        PydanticValueError, "email", PayloadSchema, status.HTTP_400_BAD_REQUEST
    )
    background_tasks(send_email, email=payload.email)

Answered By: Anubhav Gupta