pythonfastapipydantic

Having a dependency through Depends() and a schema refer to the same root level key without ending up with multiple body parameters


I have a situation where I want to authorize the active user against one of the values (Organization) in a FastAPI route. When an object of a particular type is being submitted, one of the keys (organization_id) should be present and the user should be verified as having access to the organization.

I've solved this as a dependency in the API signature to avoid having to replicate this code across all routes that needs access to this property:

def get_organization_from_body(organization_id: str = Body(None),
                               user: User = Depends(get_authenticated_user),
                               organization_service: OrganizationService = Depends(get_organization_service),
                               ) -> Organization:
    if not organization_id:
        raise HTTPException(status_code=400, detail='Missing organization_id.')

    organization = organization_service.get_organization_for_user(organization_id, user)

    if not organization:
        raise HTTPException(status_code=403, detail='Organization authorization failed.')

    return organization

This works fine, and if the API endpoint expects an organization through an organization_id key in the request, I can get the organization directly populated by introducing get_organization_from_body as a dependency in my route:

@router.post('', response_model=Bundle)
async def create_bundle([...]
                        organization: Organization = Depends(get_organization_from_body),
                        ) -> Model:
    

...and if the user doesn't have access to the organization, an 403 exception is raised.

However, I also want to include my actual object on the root level through a schema model. So my first attempt was to make a JSON request as:

{
  'name': generated_name,
  'organization_id': created_organization['id_key']
}

And then adding my incoming Pydantic model:

@router.post('', response_model=Bundle)
async def create_bundle(bundle: BundleCreate,
                        organization: Organization = Depends(get_organization_from_body),
                        [...]
                        ) -> BundleModel:
    [...]
    return bundle

The result is the same whether the pydantic model / schema contains organization_id as a field or not:

class BundleBase(BaseModel):
    name: str

    class Config:
        orm_mode = True


class BundleCreate(BundleBase):
    organization_id: str
    client_id: Optional[str]

.. but when I introduce my get_organization_from_body dependency, FastAPI sees that I have another dependency that refers to a Body field, and the description of the bundle object has to be moved inside a bundle key instead (so instead of "validating" the organization_id field, the JSON layout needs to change - and since I feel that organization_id is part of the bundle description, it should live there .. if possible).

The error message tells me that bundle was expected as a separate field:

{'detail': [{'loc': ['body', 'bundle'], 'msg': 'field required', 'type': 'value_error.missing'}]}

And rightly so, when I move name inside a bundle key instead:

{
    'bundle': {
        'name': generated_name,
    },
    'organization_id': created_organization['id_key']
}

...my test passes and the request is successful.

This might be slightly bike shedding, but if there's a quick fix to work around this limitation in any way I'd be interested to find a way to both achieve validation (either through Depends() or in some alternative way without doing it explicitly in each API route function that requires that functionality) and a flat JSON layout that matches my output format closer.


Solution

  • Prior to FastAPI 0.53.2, dependencies for the body were resolved the way you are trying to do. Such code:

    class Item(BaseModel):
        val_1: int
        val_2: int
    
    
    def get_val_1(val_1: int = Body(..., embed=True)):
         return val_1
    
    
    @app.post("/item", response_model=Item)
    def handler(full_body: Item, val_1: int = Depends(get_val_1)):
        return full_body
    

    Expected such body:

    {
        "val_1": 0,
        "val_2": 0
    }
    

    But starting from version 0.53.2, different body dependencies are automatically embedded (embed=True) and the code above expects the following body:

    {
      "full_body": {
        "val_1": 0,
        "val_2": 0
      },
      "val_1": 0
    }
    

    Now, in order to have access to the model for the whole body and to have access to its elements as a separate dependency, you need to use same dependency for the body model everywhere:

    def get_val_1(full_body: Item):
        return full_body.val_1
    
    
    @app.post("/item", response_model=Item)
    def handler(full_body: Item, val_1: int = Depends(get_val_1)):
        return full_body
    

    Update for shared dependency

    You can share one body dependency for multiple models, but in this case, two conditions must be met: the names of the dependencies must be the same and their types must be compatible (through inheritance or not). Example below:

    class Base(BaseModel):
        val_1: int
    
    
    class NotBase(BaseModel):
        val_1: int
    
    
    class Item1(Base):
        val_2: int
    
    
    class Item2(Base):
        val_3: int
    
    
    def get_val1_base(full_body: Base):
         return full_body.val_1
    
    
    def get_val1_not_base(full_body: NotBase):
        return full_body.val_1
    
    
    @app.post("/item1", response_model=Item1)
    def handler(full_body: Item1, val_1: int = Depends(get_val1_base)):
        return full_body
    
    
    @app.post("/item2", response_model=Item2)
    def handler(full_body: Item2, val_1: int = Depends(get_val1_not_base)):
        return full_body