pythonjsonfastapipydantic

Read a body JSON list with FastAPI


The body of an HTTP PUT request is a JSON list - like this:

[item1, item2, item3, ...]

I can't change this. (If the root was a JSON object rather than a list there would be no problem.)

Using FastAPI, I seem to be unable to access this content in the normal way:

@router.put('/data')
def set_data(data: DataModel): # This doesn't work; how do I even declare DataModel?

I found the following workaround, which seems like a very ugly hack:

class DataModel(BaseModel):
    __root__: List[str]


from fastAPI import Request

@router.put('/data')
async def set_data(request: Request): # Get the request object directly
    data = DataModel(__root__=await request.json())

This surely can't be the 'approved' way to achieve this. I've scoured the documentation both of FastAPI and Pydantic. What am I missing?


Solution

  • Descending from the model perspective to primitives

    In FastAPI, you derive from BaseModel to describe the data models you send and receive (i.e. FastAPI also parses for you from a body and translates to Python objects). Also, it relies on the modeling and processing from pydantic.

    from typing import List
    from pydantic import BaseModel
    
    class Item(BaseModel):
        name: str
    
    class ItemList(BaseModel):
        items: List[Item]
    
    def process_item_list(items: ItemList):
        pass
    

    This example would be able to parse JSON like

    {"items": [{"name": "John"}, {"name": "Mary"}]}
    

    In your case - depending on what shape your list entries have - you'd also go for proper type modeling, but you want to directly receive and process the list without the JSON dict wrapper around it. You could go for:

    from typing import List
    from pydantic import BaseModel
    
    class Item(BaseModel):
        name: str
    
    def process_item_list(items: List[Item]):
        pass
    

    Which is now able to process JSON like:

    [{"name": "John"}, {"name": "Mary"}]
    

    This is probably what you're looking for and the last adaption to take is depending on the shape of your item* in the list you receive. If it's plain strings, you can also go for:

    from typing import List
    
    def process_item_list(items: List[str]):
        pass
    

    Which could process JSON like

    ["John", "Mary"]
    

    I outlined the path from models down to primitives in lists because I think it's worth knowing where this can go if one needs more complexity in the data models.