14

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?

Ian Goldby
  • 5,609
  • 1
  • 45
  • 81

1 Answers1

34

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.

jbndlr
  • 4,965
  • 2
  • 21
  • 31
  • I thought I'd tried the above but obviously made some mistake along the way and ruled it out. Many thanks. – Ian Goldby Mar 25 '20 at 08:28
  • Why don't I just post the last option? Yes, that would seem better, but the API spec says that the entire list is rewritten. – Ian Goldby Mar 25 '20 at 08:29
  • Nah, that last heading was a question to myself - maybe it's poorly phrased, but I wanted to justify why I went all that way down ;) – jbndlr Mar 25 '20 at 21:34
  • 1
    i was trying too return an array without a name, as below; class ListItem(baseModel) List[Item] somehow it was working on print but cannot output . class ListItem(List[Item]) pass saved my life, thanks man – bilen May 26 '20 at 12:41
  • results in incorrect type `{items :{"items" :["name" ] } }` which is incorrect. About to try what @bilen mentioned. – Akshay Hazari Aug 01 '23 at 12:46
  • @AkshayHazari, my solution has three different options, you seem to have only picked/tried the first one. Go with the second option and you'll be fine (also see example outputs provided in my answer). – jbndlr Aug 06 '23 at 22:07
  • @jbndlr Can you explain how we can use the function while declaring the API , since it just asks for types or classes – Akshay Hazari Aug 10 '23 at 01:39