I have the following two pydantic models for a Users table in my database for a FastAPI application:
from fastapi import Form
from pydantic import BaseModel
class UserCreate(BaseModel):
username: str = Form(...)
password: str = Form(...)
class UserInDb(BaseModel):
id: int
username:str
hashed_password: str
I use the UserCreate class to get form data needed from the client to create a user and then I map it to UserInDb. This is how I initially implemented it:
password = user_create_instance.password
hashed_password = some_hashing_function(password)
user_in_db = UserInDb(**user_create_instance.dict(), hashed_password=hashed_password)
This however throws a value_error.missing error because user_create_instance does not have an id parameter which UserInDb expects to be passed in but the value for that comes from the database.
This issue is fixable by doing something like id: int = None when defining the attribute in the class but it doesn't feel right to do it this way. Is there a better approach?
Related
This question already has answers here:
FastAPI - GET request results in typeerror (value is not a valid dict)
(2 answers)
Closed 10 months ago.
I started to build a wepapp with fastapi and Vue. I started at the backend and tried to get some data from a database. As ORM I use SQLAlchemy. What I am tring to achive is to strip down the DB response to some specific columns.
I am fairly new to the world of webapplications, I will also appreciate some good resources. I find it kind of hard to get started in this topic. Despite there being alt of Tutorials, they just cover how to start a basic site and leave the rest to the docs. But I you are not used to the terminology, its quite easy to get lost there.
Anyway, I have this test setup:
My model for the db is
class System(Base):
__tablename__ = 'system'
id = Column(BIGINT(20), primary_key=True)
name = Column(String(200), nullable=False)
type = Column(String(200), nullable=False)
installed_power = Column(Float(asdecimal=True), nullable=False)
date_of_installation = Column(
DateTime, nullable=False, server_default=text("current_timestamp()"))
last_changed = Column(DateTime, nullable=False, server_default=text(
"current_timestamp() ON UPDATE current_timestamp()"))
site_id = Column(ForeignKey('site.id'), nullable=False, index=True)
site = relationship('Site')
and my schema is
class System(BaseModel):
id: int
name: str
type: str
installed_power: int
In my main.py I am doing this
#app.get("/system", response_model=schemas.System)
def get_systems(db: Session = Depends(get_db)):
query = crud.get_system(db)
return query.all()
This dose not work. The error says pydantic.error_wrappers.ValidationError: 1 validation error for System response value is not a valid dict (type=type_error.dict)
If I add all db columns to the schema it works obviously
I also tried something like this, but this did not work either.
#app.get("/system", response_model=schemas.System)
def get_systems(db: Session = Depends(get_db)):
query = crud.get_system(db)
return query.all()
res = []
for row in query:
system = schemas.System()
system.id = row.id
system.name = row.name
system.type = row.type
system.installed_power = row.installed_power
res.append(system)
return res
WITHOUT orm_mode
In your first example, your response_model expects a schema (System), but you send it query.all() which returns a list containing classes from the model you get. (SQLAlchemy does not return a dictionary, which is what pydantic expects by default).
So, at this point, you have to make a choice, either your endpoint must return ONE object, in which case you should not use query.all() but something like query.one_or_none().
Or you want to return a list of objects and your response_model should be:
from typing import List
#app.get("/system", response_model=List[schemas.System])
All that remains is to format your data so that it corresponds to your schema.
for version with only one data:
#app.get("/system", response_model=schemas.System)
def get_systems(db: Session = Depends(get_db)):
query = crud.get_system(db).one_or_none()
return schemas.System(id=query.id,name= query.name, type=query.type, installed_power=query.installed_power)
for version with multiple datas:
from typing import List
def make_response_systems(query):
result = []
for data in query:
result.append(schemas.System(id=query.id,name= query.name, type=query.type, installed_power=query.installed_power))
return result
#app.get("/system", response_model=List[schemas.System])
def get_systems(db: Session = Depends(get_db)):
query = crud.get_system(db).all()
return make_response_systems(query)
There are more aesthetic ways to do this. But I think the example above is a good way to understand how it works.
For the rest, you can look at the orm_mode of pydantics models, which you can find in the FastAPI documentation.
To learn, the FastAPI documentation is very complete and easy to access.
WITH orm_mode
Pydantic's orm_mode will tell the Pydantic model to read the data even if it is not a dict, but an ORM model (or any other arbitrary object with attributes).
class System(BaseModel):
id: int
name: str
type: str
installed_power: int
class Config:
orm_mode = True
#app.get("/system", response_model=schemas.System)
def get_systems(db: Session = Depends(get_db)):
query = crud.get_system(db).one_or_none()
return query
Documentation fastapi orm_mode : https://fastapi.tiangolo.com/tutorial/sql-databases/?h=orm_mode#use-pydantics-orm_mode
As per pydantic docs
ORM Mode (aka Arbitrary Class Instances)🔗
Pydantic models can be created from arbitrary class instances to support models that map to ORM objects.
To do this:
The Config property orm_mode must be set to True.
The special constructor from_orm must be used to create the model instance.
we need to add orm_mode to the schema config.
class System(BaseModel):
id: int
name: str
type: str
installed_power: int
class Config:
orm_mode = True
Reference: https://pydantic-docs.helpmanual.io/usage/models/#orm-mode-aka-arbitrary-class-instances
Currently I have schema with field "name". I use constr to specify it
from pydantic import BaseModel, constr
class MySchema(BaseModel):
name: constr(strict=True, min_length=1, max_length=50)
I want to use pydantic StrictStr type like this:
from pydantic import BaseModel, StrictStr, Field
class MySchema(BaseModel):
name: StrictStr = Field(min_length=1, max_length=50)
But it raises error:
E ValueError: On field "name" the following field constraints are set but not enforced: max_length, min_length.
E For more details see https://pydantic-docs.helpmanual.io/usage/schema/#unenforced-field-constraints
In docs, as i understand, it advices to use use raw attribute name like maxLength isntead of max_length(like exclusiveMaximum for int) but this constraints are not enforced so validations are not applied.
My question is: How I can use StrictStr type for name field and apply native validations like min_length, max_length?
You have multiple options here, either you create a new type based on StrictString, or you inherit from StrictString or you use constr with strict set to True. Creating a type as done below does the same as inheriting from StrictString, just a different syntax if you want. That should all give you the necessary type validations. In code, that would read like
MyStrictStr1 = type('MyStrictStr', (StrictStr,), {"min_length":1, "max_length":5})
class MyStrictStr2(StrictStr):
min_length = 1
max_length = 5
MyStrictStr3 = constr(min_length=1, max_length=5, strict=True)
I'm trying to build a Python FastAPI blog system using SQLAlchemy with SQLite and am having problems using/understanding the response_model parameter of the API decorator. Here's a SQLAlchemy model:
class User(SqlAlchemyBase):
__tablename__ = 'user'
__table_args__ = {"keep_existing": True}
user_uid: int = sa.Column(GUID, primary_key=True, default=GUID_DEFAULT_SQLITE)
first_name: str = sa.Column(sa.String)
last_name: str = sa.Column(sa.String)
email: str = sa.Column(sa.String, index=True, unique=True)
posts: List["Post"] = relationship("Post", backref="user")
created: datetime = sa.Column(sa.DateTime, default=datetime.now(tz=timezone.utc), index=True)
updated: datetime = sa.Column(sa.DateTime, default=datetime.now(
tz=timezone.utc), onupdate=datetime.now(tz=timezone.utc), index=True)
Here's the Pydantic schema's for a User:
from datetime import datetime
from pydantic import BaseModel
class UserBase(BaseModel):
first_name: str
last_name: str
email: str
class UserInDB(UserBase):
user_uid: int
created: datetime
updated: datetime
class Config:
orm_mode = True
class User(UserInDB):
pass
Here's a ULR endpoint to get a single user that has the response_model parameter included:
from typing import List
import fastapi
from starlette import status
from backend.schema.user import User
from backend.services import user_service
router = fastapi.APIRouter()
#router.get("/api/users/{user_uid}", response_model=User, status_code=status.HTTP_200_OK)
async def get_one_user(user_uid: str = None) -> User:
return await user_service.get_user(user_uid)
If I execute the above call in the OpenAPI docs created by FastAPI I get an internal server error in the OpenAPI docs and these errors in the console that's running the FastAPI application:
File "/Users/dougfarrell/projects/blog/.venv/lib/python3.10/site-packages/fastapi/routing.py", line 137, in serialize_response
raise ValidationError(errors, field.type_)
pydantic.error_wrappers.ValidationError: 6 validation errors for User
response -> first_name
field required (type=value_error.missing)
response -> last_name
field required (type=value_error.missing)
response -> email
field required (type=value_error.missing)
response -> user_uid
field required (type=value_error.missing)
response -> created
field required (type=value_error.missing)
response -> updated
field required (type=value_error.missing)
If I take the response_model parameter out of the #router.get(...) decorator I get back the results of the SQLAlchemy query, but no Pydantic serialization. I don't know what the error stack trace is trying to tell me. Can anyone offer some suggestions, advice or resources that might help me figure out what I'm doing wrong?
I'm using Python version 3.10.1, FastAPI version 0.70.1 and SQLAlchemy version 1.4.29.
Thanks!
Try this:
from typing import List
and change response_model=User to:
response_model=List[User]
I'm building a simple REST API.
If the object you want to update contains properties that can contain null, what is the best way to define it in fastapi?
When using pydantic.BaseModel, it is not possible to support the usage of not updating if the property does not exist.
example:
from fastapi import Body, FastAPI
from typing import Optional, Literal
import dataclasses
app = FastAPI()
#dataclasses.dataclass
class User:
name: str
type: Optional[Literal['admin', 'guest']]
user = User('test_user', 'admin')
class UpdateUser(BaseModel):
name: str
type: Optional[Literal['admin', 'guest']]
#app.put('/')
def put(update_user: UpdateUser):
# In the case of BaseModel, I don't know the difference between the property
# that I don't update and the property that I want to update with None,
# so I always update with None.
user.name = update_user.name
user.type = update_user.type
I think the simplest way is to use dict.
example:
from fastapi import Body, FastAPI
from typing import Optional, Literal
import dataclasses
app = FastAPI()
#dataclasses.dataclass
class User:
id: int
name: str
type: Optional[Literal['admin', 'guest']]
user = User(1, 'test_user', 'admin')
#app.put('/')
def put(body = Body(...)):
if 'name' in body:
user.name = body.name
if 'type' in body:
user.type = body.type
However, in this case, it is not possible to specify the JSON type used for the request like BaseModel.
How can I implement the update process with dict-like flexibility while preserving type information?
It turns out that this is a typical patch request.
At the time of update, UpdateUser can receive it, and update_user.dict(exclude_unset=True) can receive a dictionary containing only the parts that need to be updated.
example:
from fastapi import Body, FastAPI
from typing import Optional, Literal
import dataclasses
app = FastAPI()
#dataclasses.dataclass
class User:
name: str
type: Optional[Literal['admin', 'guest']]
user = User('test_user', 'admin')
class UpdateUser(BaseModel):
name: str
type: Optional[Literal['admin', 'guest']]
#app.patch('/')
def patch(update_user: UpdateUser):
update_user_dict = update_user.dict(exclude_unset=True)
if 'name' in update_user_dict:
user.name = update_user.name
if 'type' in update_user_dict:
user.type = update_user.type
https://fastapi.tiangolo.com/tutorial/body-updates/#using-pydantics-exclude_unset-parameter
I'm handling this request in my code (Python3.9, FastAPI, Pydantic):
https://myapi.com/api?params[A]=1¶ms[B]=2
I tried to make following model:
BaseModel for handling special get request
(for fastapi.Query and pydantic.Field is same)
I also set up aliases for it, but in swagger docs I see next field:
Snap of the swagger docs
There are fields that are specified as extra_data
So, if I specify query params in parameters of my endpoint like this:
#app.get('/')
def my_handler(a: str = Query(None, alias="params[A]")):
return None
Everything works fine. How can I fix it? I want to initialize my pydantic.BaseModel with speacial aliases using this way and avoid usage of query-params in
class MyModel(BaseModel):
a = Field(alias="params[A]")
b = Field(alias="params[B]")
def my_handler(model: MyModel = Depends()):
return model.dict()