Search code examples
pythonsqlalchemyschemapydantic

How to add second Pydantic schema into Fastapi endpoint as an option


Ok. Guys I can't figure it out. Is any option to add second Pydantic schema option, or switch between 2 Pydantic schemas according of parameter in request? What I want to archieve. If paremeter equals 'all' I would like to add 'date' field into schema. And if parameter is 'None' I would like to exclude 'data' field at all. With code below with no parameter at all got: "date": null

[
  {
    "unit": "Xeikon 1",
    "color": 65817763,
    "bw": 552903,
    "date": null
  },
  {
    "unit": "Xeikon 2",
    "color": 65825687,
    "bw": 552903,
    "date": null
  },
  {
    "unit": "Xeikon 3",
    "color": 65825784,
    "bw": 552903,
    "date": null
  }
]

This is part of my code

class Clicks(BaseModel):
    unit: str
    color: int
    bw: int
    date: Optional[date]
    class Config:
        orm_mode = True

@xeikon_api.get('/xeikon/clicks/', response_model=List[schema.Clicks])
async def xeikon_Clicks_data(details: Optional[str]=None) :
    """
     endpoint: list all Clicks data \n
    """
    try:
        if details == 'all':
            return db.session.query(Clicks).all()
        if details is None:
            from main import session_local
            response = session_local.query(Clicks)
            return reorganizeClicksData(response)
    except NoResultFound as n:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=n.orig.args,
            ) from n

Solution

  • The way you describe it, that would be two distinct response schemas. Therefore you would have to define two distinct models for the response.

    Let me quote the relevant section of the FastAPI docs:

    You can declare a response to be the Union of two types, that means, that the response would be any of the two. It will be defined in OpenAPI with anyOf. To do that, use the standard Python type hint typing.Union.

    To simplify your example a bit, here is a possible solution:

    from datetime import date
    from typing import Optional, Union
    
    from fastapi import FastAPI, HTTPException
    from pydantic import BaseModel
    
    class Clicks(BaseModel):
        unit: str
        color: int
        bw: int
    
        class Config:
            orm_mode = True
    
    class ExtendedClicks(Clicks):
        date: date
    
    AnyClicks = Union[list[ExtendedClicks], list[Clicks]]
    
    app = FastAPI()
    
    @app.get("/clicks")
    async def endpoint(details: Optional[str] = None) -> AnyClicks:
        # Get the clicks data:
        test_data = [
            {"unit": "foo", "color": 1, "bw": 1, "date": "2023-05-10"},
            {"unit": "bar", "color": 0, "bw": 0, "date": "2023-01-01"},
        ]
        if details == "all":
            return [ExtendedClicks.parse_obj(obj) for obj in test_data]
        if details is None:
            return [Clicks.parse_obj(obj) for obj in test_data]
        raise HTTPException(400)
    

    The order in the type union is important. Since ExtendedClicks is a subtype of Clicks and it simply ignores extra values, if we were to switch the order in AnyClicks, the response would always match Clicks and you would never see the date field.

    The response in this example with the details=all query:

    [
      {
        "unit": "foo",
        "color": 1,
        "bw": 1,
        "date": "2023-05-10"
      },
      {
        "unit": "bar",
        "color": 0,
        "bw": 0,
        "date": "2023-01-01"
      }
    ]
    

    With no query parameters:

    [
      {
        "unit": "foo",
        "color": 1,
        "bw": 1
      },
      {
        "unit": "bar",
        "color": 0,
        "bw": 0
      }
    ]
    

    I would still recommend not doing that, i.e. not using a union of return types. There is no way to express via the OpenAPI schema that the response schema depends on specific query parameters. So all that one can see from the endpoint schema is that it may return a list of Clicks and it also may return a list of ExtendedClicks.

    From an API design standpoint I would recommend just having two separate endpoints for the two distinct schemas.