Read a body JSON list with FastAPI

Ian Goldby picture Ian Goldby · Mar 25, 2020 · Viewed 8k times · Source

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?

Answer

jbndlr picture jbndlr · Mar 25, 2020

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.