I am using FastAPI to write a web service. It is good and fast.
FastAPI is using pydantic models to validate input and output data, everything is good but when I want to declare a nested model for array of jsons like below:
[
{
"name": "name1",
"family": "family1"
},
{
"name": "name2",
"family": "family2"
}
]
I get empty response.
I think there is a problem with my model which is:
class Test(BaseModel):
name: str
family: str
class Config:
orm_mode = True
class Tests(BaseModel):
List[Test]
class Config:
orm_mode = True
So, my question is how should I write a model for array of jsons?
Update (26/09/2020)
In Python 3.9 (not yet released), you can do the same as below but with the built-in list generic type (which is always in scope) rather than needing to import the capitalized List type from typing, e.g.
#app.get("/tests", response_model=list[Test])
The issue here is that you are trying to create a pydantic model where it is not needed. If you want to serialize/deserialize a list of objects, just wrap your singular model in a List[] from python's builtin typing module. There is no need to try to create a plural version of your object with a pydantic BaseModel (and as you can see, it does not work anyway).
With that said, the simplest way to do what you want is to just specify a List[Test] at any point where you need a list of Tests, e.g.
from typing import List
from fastapi import FastAPI
from pydantic import BaseModel
existing_tests = [
{
"name": "name1",
"family": "family1"
},
{
"name": "name2",
"family": "family2"
}
]
class Test(BaseModel):
name: str
family: str
class Config:
orm_mode = True
app = FastAPI()
#app.get("/tests", response_model=List[Test])
async def fetch_tests():
return existing_tests
#app.post("/tests")
async def submit_tests(new_tests: List[Test]):
print(new_tests)
But of course if you find yourself repeatedly (or only) specifying Test as a list, you can of course just assign this to a variable and then use that variable where needed, like so:
Tests = List[Test]
#app.get("/tests", response_model=Tests)
async def fetch_tests():
return existing_tests
#app.post("/tests")
async def submit_tests(new_tests: Tests):
print(new_tests)
I think the first option is probably slightly clearer in your code though, and unless you are specifying List[Test] many times, using a variable for this purpose is probably not worth the extra layer of indirection.
Related
I cannot figure it out how to display a one to many relationship using fastapi and sqlmodel. I've read through this question but my case seems to be slightly different. Specially in the function call.
This is my schemas.py:
from typing import Optional
from sqlmodel import Field, Relationship, SQLModel
class BinaryBase(SQLModel):
product_id: int
software_install_path: Optional[str] = None
host_id: int = Field(foreign_key="host.id", nullable=False)
class Binary(BinaryBase, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
host_id: int = Field(foreign_key="host.id", nullable=False)
host: "Host" = Relationship(back_populates="binaries")
class HostBase(SQLModel):
name: str
region: Optional[str] = None
os_version: Optional[str] = None
network: Optional[int] = None
class Host(HostBase, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
binaries: list[Binary] = Relationship(back_populates='host')
class HostReadWithBinary(HostBase):
bins: list[HostBase] = []
class BinaryReadWithHost(BinaryBase):
host: BinaryBase
And this is my main.py:
from fastapi import Depends, FastAPI
from sqlmodel import Session, col, select
...
#app.get(
"/binaries/",
response_model=list[HostReadWithBinary]
)
def get_binary(name: Optional[str] = None, session: Session = Depends(get_session)) -> list[HostReadWithBinary]:
query = select(Host).limit(100)
if name:
query = query.where(col(Host.name).contains(name.lower()))
return session.exec(query).all()
The Host table represents the 1 part and the Binary table represents the many part. I would like to get a response of all the BinaryBase attributes for all the hosts eagerly. But what I get is this:
[
{
"name": "hkl20014889",
"region": "HK",
"os_version": "Red Hat 6.10",
"network": 3,
"bins": []
},
{
"name": "hkl20016283",
"region": "HK",
"os_version": "Red Hat 6.10",
"network": 3,
"bins": []
},
....
Theoreticaly bins should hold the attributes of the Host table when id in Host joins host_id in Binary.
You need to realize that when you define a response_model for a route, it will always try to parse whatever data comes out of your route handler function (get_binary in this case) through that model. This is done by essentially calling the .from_orm method on the response model, which goes through all fields defined on it and tries to find corresponding attributes (i.e. with the same names) on the object you pass to it.
The model you specified (in a list, but the argument stands) is HostReadWithBinary. Aside from the fields defined on its parent model HostBase it only has the field bins, which is supposed to be a list of HostBase.
First of all, I think you meant to declare that bins field to be of the type list[BinaryBase], not list[HostBase]. If you had named the field correctly, this would have caused an error, but this is where your second mistake comes in.
You also named the field on your response model bins, but your handler function performs a query that returns a list of Host model instances. That model does not have a bins field. It has a field named binaries. This means that when the from_orm method gets to the HostReadWithBinary.bins field, it checks if the corresponding Host instance has an attribute named bins. It does not find one, but no problem because you set a default for HostReadWithBinary.bins, namely the empty list [], so that is what is set on each of the resulting response model instances.
You should therefore be able to fix your error by changing the response model definition like this:
class HostReadWithBinary(HostBase):
binaries: list[BinaryBase] = []
Alternatively, you can change the name of the relationship field on your Host model:
class Host(HostBase, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
bins: list[Binary] = Relationship(back_populates='host')
class HostReadWithBinary(HostBase):
bins: list[BinaryBase] = []
They need to be the same, otherwise parsing an object of one model to the other will not work (properly).
Side note: You also mistakenly annotated the host field on BinaryReadWithHost with BinaryBase instead of HostBase.
PS: I also just noticed a minor mistake related to type annotations. You declare the return type of your route handler function to be list[HostReadWithBinary], but that is not what it returns. It returns list[Host]. This is part of the misunderstanding with the response models. The decorated version of your route is what returns list[HostReadWithBinary]. Your route handler get_binary by itself (i.e. before decoration) returns list[Host], which is then passed to the wrapper around it and that wrapper parses it to list[HostReadWithBinary] and sends that data on its way (to the client eventually). This wrapper action obviously happens behind the scenes and is part of that FastAPI decorator magic.
I'm using pydantic 1.9.0 and fastapi 0.71.0 with Python 3.7.8. Here's how I've defined my model:
class PartModel(BaseModel):
_id: str
_key: str
number: str = Field(...)
name: str = Field(...)
price: int = Field(...)
class Config:
schema_extra = {
"example": {
"_key": "12453",
"number": "5F22",
"name": "SHA9-169",
"price": 4,
}
}
I would like to be flexible with the Model because my dictionaries are not consisted when it comes to the length. For example one dictionary might have additional key/value pairs. At the moment when i try to make the request through the FastApi it doesn't allow me to POST in the Database. Is there any way to keep my Model flexible/extendable without defining every key in the class?
In order to work with flexible models, I would recommend to work with Optional fields, as described in https://pydantic-docs.helpmanual.io/usage/models/#required-optional-fields
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.
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
I want to validate JSON object (it is in Telegram Bot API) which contains from field (which is reserved word in Python) by using pydantic validator. So my model should look like the following:
class Message(BaseModel):
message_id: int
from: Optional[str]
date: int
chat: Any
...
But using from keyword is not allowed in this context.
How could I do this?
Note: this is different than "Why we can't use keywords as attributes" because here we get external JSON we don't control and we anyway should handle JSON with from field.
I believe you can replace from with from_.
You can do it like this:
class Message(BaseModel):
message_id: int
from_: Optional[str]
date: int
chat: Any
class Config:
fields = {
'from_': 'from'
}
...
There might be a way to do this using a class statement, but I didn't see anything in a quick skim of the documentation. What you could do is use dynamic model creation instead.
fields = {
'message_id': (int,),
'from': (Optional[str], ...),
'date': (int, ...),
'chat': (Any, ...)
}
Message = create_model("Message", **fields)
How do I override the JSON encoder used the marshmallow library so that it can serialize a Decimal field?I think I can do this by overriding json_module in the base Schema or Meta class, but I don't know how:
https://github.com/marshmallow-code/marshmallow/blob/dev/marshmallow/schema.py#L194
I trawled all the docs and read the code, but I'm not a Python native.
If you want to serialize a Decimal field (and keep the value as a number), you can override the default json module used by Marshmallow in its dumps() call to use simplejson instead.
To do this, just add a class Meta definition to your schema, and specify a json_module property for this class.
Example:
import simplejson
class MySchema(Schema):
amount = fields.Decimal()
class Meta:
json_module = simplejson
Then, to serialize:
my_schema = MySchema()
my_schema.dumps(my_object)
I think the solution is to use marshmallow.fields.Decimal with as_string=True:
This field serializes to a decimal.Decimal object by default. If you
need to render your data as JSON, keep in mind that the json module
from the standard library does not encode decimal.Decimal. Therefore,
you must use a JSON library that can handle decimals, such as
simplejson, or serialize to a string by passing as_string=True.
I had the same issue and I endup changing the field on Schema to string. In my case, since I'm only going to return it in json, it really doesn't matter if it is string or decimal.
from marshmallow_sqlalchemy import ModelSchema
from marshmallow import fields
class CurrencyValueSchema(ModelSchema):
class Meta:
model = CurrencyValue
value = fields.String()
My returned json:
{
"currency_values": [
{
"id": 9,
"timestamp": "2016-11-18T23:59:59+00:00",
"value": "0.944304"
},
{
"id": 10,
"timestamp": "2016-11-18T23:59:59+00:00",
"value": "3.392204"
},
}